mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Merge pull request #4486 from dotCooCoo/feat/text-styles-select-all-unused
Text styles: Select All Unused highlights the unused styles, and Delete deletes them all
This commit is contained in:
@@ -99,10 +99,18 @@ macro_rules! style_cmds {
|
||||
concat!($p, ".delete"),
|
||||
concat!("Delete ", $what, " Style"),
|
||||
&["Window", "Type", $menu],
|
||||
"{name} (text keeps its look)",
|
||||
"{name} | {names: [name…]} delete styles in one undo step (text keeps its look; never the Normal style)",
|
||||
|s, p| delete(s, p, $kind),
|
||||
true,
|
||||
),
|
||||
spec(
|
||||
concat!($p, ".unused"),
|
||||
"Select All Unused",
|
||||
&["Window", "Type", $menu],
|
||||
"{} → {names: [the styles no text uses, in panel order; never the Normal style]} (the panel highlights them)",
|
||||
|s, _| unused(s, $kind),
|
||||
false,
|
||||
),
|
||||
]
|
||||
};
|
||||
}
|
||||
@@ -291,21 +299,33 @@ fn text_mut(d: &mut Document, id: NodeId) -> Option<&mut vectorcraft_doc::TextOb
|
||||
}
|
||||
}
|
||||
|
||||
/// How many runs (char) or paragraphs (para) use `name`.
|
||||
fn uses(d: &Document, kind: Kind, name: &str) -> usize {
|
||||
let target = (name != kind.normal()).then_some(name);
|
||||
let mut n = 0;
|
||||
d.walk(|node| {
|
||||
if let NodeKind::Text(t) = &node.kind {
|
||||
n += match kind {
|
||||
Kind::Char => t.runs.iter().filter(|r| r.style.style_name.as_deref() == target).count(),
|
||||
Kind::Para => (0..t.paragraph_count()).filter(|&i| t.para_at(i).style_name.as_deref() == target).count(),
|
||||
};
|
||||
}
|
||||
});
|
||||
/// How many runs (char) or paragraphs (para) use each style, by style name (`None`: the Normal
|
||||
/// style). Type in the art (opacity masks included), symbols, graph designs, pattern tiles and the
|
||||
/// art of brushes counts.
|
||||
fn uses(d: &Document, kind: Kind) -> std::collections::HashMap<Option<String>, usize> {
|
||||
let mut n = std::collections::HashMap::new();
|
||||
let mut count = |name: &Option<String>| *n.entry(name.clone()).or_insert(0) += 1;
|
||||
let brushes = vectorcraft_brush::library(d);
|
||||
let trees = d.layers.iter().map(|l| &**l).chain(d.symbols.iter().map(|s| &*s.art)).chain(d.graph_designs.iter().map(|g| &*g.art));
|
||||
let trees = trees.chain(d.patterns.iter().flat_map(|p| p.art.iter().map(|a| &**a))).chain(brushes.iter().flat_map(|b| b.art()));
|
||||
for tree in trees {
|
||||
vectorcraft_doc::swatches::walk_with_masks(tree, &mut |node| {
|
||||
if let NodeKind::Text(t) = &node.kind {
|
||||
match kind {
|
||||
Kind::Char => t.runs.iter().for_each(|r| count(&r.style.style_name)),
|
||||
Kind::Para => (0..t.paragraph_count()).for_each(|i| count(&t.para_at(i).style_name)),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// How many runs (char) or paragraphs (para) use `name` (see [`uses`]).
|
||||
fn uses_of(uses: &std::collections::HashMap<Option<String>, usize>, kind: Kind, name: &str) -> usize {
|
||||
uses.get(&(name != kind.normal()).then(|| name.to_string())).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Update every user of `name` from `old` to `new` attributes (overrides kept).
|
||||
fn update_users(d: &mut Document, kind: Kind, name: &str, old: &Map<String, Value>, new: &Map<String, Value>) {
|
||||
let target = (name != kind.normal()).then_some(name.to_string());
|
||||
@@ -434,7 +454,8 @@ fn list(s: &mut Session, kind: Kind) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let mut names = vec![kind.normal().to_string()];
|
||||
names.extend(kind.defs(d).iter().map(|s| s.name.clone()).filter(|n| n != kind.normal()));
|
||||
let styles: Vec<Value> = names.iter().map(|n| json!({ "name": n, "attrs": kind.attrs(d, n), "uses": uses(d, kind, n) })).collect();
|
||||
let uses = uses(d, kind);
|
||||
let styles: Vec<Value> = names.iter().map(|n| json!({ "name": n, "attrs": kind.attrs(d, n), "uses": uses_of(&uses, kind, n) })).collect();
|
||||
Ok(json!({ "styles": styles }))
|
||||
}
|
||||
|
||||
@@ -616,16 +637,26 @@ fn rename(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
|
||||
|
||||
fn delete(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
|
||||
let c = kind.cmd("delete");
|
||||
let name = name_param(p, kind, "delete")?.to_string();
|
||||
if name == kind.normal() || !kind.defs(&s.doc()?.doc).iter().any(|s| s.name == name) {
|
||||
let names: Vec<String> = match p.get("names") {
|
||||
Some(v) => v
|
||||
.as_array()
|
||||
.and_then(|a| a.iter().map(|n| n.as_str().map(str::to_string)).collect::<Option<Vec<_>>>())
|
||||
.ok_or_else(|| bad(&c, "`names` must be an array of style names"))?,
|
||||
None => vec![name_param(p, kind, "delete")?.to_string()],
|
||||
};
|
||||
if names.is_empty() {
|
||||
return Err(bad(&c, "missing `name`"));
|
||||
}
|
||||
let defs = kind.defs(&s.doc()?.doc);
|
||||
if let Some(name) = names.iter().find(|n| *n == kind.normal() || !defs.iter().any(|s| s.name == **n)) {
|
||||
return Err(bad(&c, format!("`{name}` can't be deleted")));
|
||||
}
|
||||
s.edit("Delete Style", |d, _| {
|
||||
kind.defs_mut(d).retain(|s| s.name != name);
|
||||
s.edit(if names.len() > 1 { "Delete Styles" } else { "Delete Style" }, |d, _| {
|
||||
kind.defs_mut(d).retain(|s| !names.contains(&s.name));
|
||||
for id in all_text(d) {
|
||||
let Some(t) = text_mut(d, id) else { continue };
|
||||
let fix = |n: &mut Option<String>| {
|
||||
if n.as_deref() == Some(name.as_str()) {
|
||||
if n.as_ref().is_some_and(|n| names.contains(n)) {
|
||||
*n = None;
|
||||
}
|
||||
};
|
||||
@@ -639,5 +670,14 @@ fn delete(s: &mut Session, p: &Value, kind: Kind) -> Result<Value> {
|
||||
ok()
|
||||
}
|
||||
|
||||
/// The styles no text uses (see [`uses`]), in the order the panel lists them, without the Normal
|
||||
/// style.
|
||||
fn unused(s: &mut Session, kind: Kind) -> Result<Value> {
|
||||
let d = &s.doc()?.doc;
|
||||
let uses = uses(d, kind);
|
||||
let names: Vec<&str> = kind.defs(d).iter().map(|s| s.name.as_str()).filter(|n| *n != kind.normal() && uses_of(&uses, kind, n) == 0).collect();
|
||||
Ok(json!({ "names": names }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -122,6 +122,100 @@ fn new_styles_trim_their_names_as_a_rename_does() {
|
||||
assert_eq!(s.execute("charStyle.duplicate", &json!({"name": " Old "})).unwrap()["name"], "Old copy 2");
|
||||
}
|
||||
|
||||
fn unused(s: &mut Session, p: &str) -> Vec<String> {
|
||||
serde_json::from_value(s.execute(&format!("{p}.unused"), &json!({})).unwrap()["names"].clone()).unwrap()
|
||||
}
|
||||
|
||||
fn listed(s: &mut Session, p: &str) -> Vec<String> {
|
||||
let list = s.execute(&format!("{p}.list"), &json!({})).unwrap();
|
||||
list["styles"].as_array().unwrap().iter().filter_map(|x| x["name"].as_str()).map(str::to_string).collect()
|
||||
}
|
||||
|
||||
/// Select All Unused: `charStyle.unused` and `paraStyle.unused` list, in panel order and without an
|
||||
/// undo step, the styles no text uses, never the Normal style. Type in a symbol, an opacity mask, a
|
||||
/// graph design, a pattern tile or a brush's art counts, as type in the layers does.
|
||||
#[test]
|
||||
fn unused_styles_are_those_no_text_uses_wherever_the_text_is() {
|
||||
let (mut s, id) = session_with_text("Used");
|
||||
let names = ["Spare", "Used", "Symbol", "Mask", "Design", "Pattern", "Brush", "Also spare"];
|
||||
for p in ["charStyle", "paraStyle"] {
|
||||
for n in names {
|
||||
s.execute(&format!("{p}.new"), &json!({"name": n, "attrs": {}})).unwrap();
|
||||
}
|
||||
s.execute(&format!("{p}.apply"), &json!({"name": "Used", "id": id})).unwrap();
|
||||
}
|
||||
// Type in style `name`, selected.
|
||||
let styled = |s: &mut Session, name: &str| {
|
||||
let t = s.execute("text.create", &json!({"x": 10, "y": 150, "text": name})).unwrap()["id"].as_u64().unwrap();
|
||||
for p in ["charStyle", "paraStyle"] {
|
||||
s.execute(&format!("{p}.apply"), &json!({"name": name, "id": t})).unwrap();
|
||||
}
|
||||
s.execute("select.set", &json!({"ids": [t]})).unwrap();
|
||||
NodeId(t)
|
||||
};
|
||||
let clear = |s: &mut Session, id: NodeId| {
|
||||
s.execute("edit.clear", &json!({"ids": [id.0]})).unwrap();
|
||||
};
|
||||
styled(&mut s, "Symbol");
|
||||
let sym = s.execute("symbol.new", &json!({"name": "Mark"})).unwrap();
|
||||
clear(&mut s, NodeId(sym["id"].as_u64().unwrap()));
|
||||
let masked = s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 60, "height": 60})).unwrap()["id"].as_u64().unwrap();
|
||||
let mask = styled(&mut s, "Mask");
|
||||
s.execute("select.set", &json!({"ids": [masked, mask.0]})).unwrap();
|
||||
s.execute("transparency.makeOpacityMask", &json!({})).unwrap();
|
||||
let design = styled(&mut s, "Design");
|
||||
s.execute("graph.design", &json!({"new": "Bar"})).unwrap();
|
||||
clear(&mut s, design);
|
||||
let tile = styled(&mut s, "Pattern");
|
||||
s.execute("object.pattern.make", &json!({"edit": false})).unwrap();
|
||||
if s.doc().unwrap().doc.node(tile).is_some() {
|
||||
clear(&mut s, tile);
|
||||
}
|
||||
let art = styled(&mut s, "Brush");
|
||||
s.execute("brush.new", &json!({"type": "art", "name": "Lettering"})).unwrap();
|
||||
clear(&mut s, art);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
for p in ["charStyle", "paraStyle"] {
|
||||
assert_eq!(unused(&mut s, p), ["Spare", "Also spare"], "{p}");
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
|
||||
let list = s.execute("charStyle.list", &json!({})).unwrap();
|
||||
let uses = |name: &str| list["styles"].as_array().unwrap().iter().find(|x| x["name"] == name).unwrap()["uses"].as_u64();
|
||||
for name in ["Symbol", "Mask", "Design", "Pattern", "Brush"] {
|
||||
assert_eq!(uses(name), Some(1), "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `charStyle.delete` and `paraStyle.delete` take `names` and delete them in one undo step; text
|
||||
/// that used them keeps its look. A name that is not a style, the Normal style, or `names` that is
|
||||
/// not an array of strings deletes none.
|
||||
#[test]
|
||||
fn several_styles_are_deleted_in_one_step() {
|
||||
let (mut s, id) = session_with_text("Title");
|
||||
s.execute("text.setStyle", &json!({"size": 40})).unwrap();
|
||||
for p in ["charStyle", "paraStyle"] {
|
||||
for n in ["A", "B", "C"] {
|
||||
s.execute(&format!("{p}.new"), &json!({"name": n})).unwrap();
|
||||
}
|
||||
s.execute(&format!("{p}.apply"), &json!({"name": "B", "id": id})).unwrap();
|
||||
let before = listed(&mut s, p);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let normal = before[0].clone();
|
||||
for bad in [json!(["A", "Nope"]), json!(["A", normal]), json!([]), json!(["A", 7]), json!("A")] {
|
||||
assert!(s.execute(&format!("{p}.delete"), &json!({"names": bad})).is_err(), "{p} {bad}");
|
||||
}
|
||||
assert_eq!((listed(&mut s, p), s.doc().unwrap().history.undo.len()), (before.clone(), undo), "{p}");
|
||||
s.execute(&format!("{p}.delete"), &json!({"names": ["A", "B"]})).unwrap();
|
||||
assert_eq!(listed(&mut s, p), [normal.as_str(), "C"], "{p}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "{p}");
|
||||
assert_eq!(s.execute("edit.undo", &json!({})).unwrap()["undone"], "Delete Styles", "{p}");
|
||||
assert_eq!(listed(&mut s, p), before, "{p}");
|
||||
}
|
||||
s.execute("charStyle.delete", &json!({"names": ["B"]})).unwrap();
|
||||
assert_eq!((runs(&s, id)[0].style.style_name.as_deref(), runs(&s, id)[0].style.size), (None, 40.0));
|
||||
assert_eq!(s.execute("edit.undo", &json!({})).unwrap()["undone"], "Delete Style");
|
||||
}
|
||||
|
||||
/// #1001: attributes their JSON leaves out at the defaults (Auto leading and kerning, no
|
||||
/// mojikumi) are still attributes a style can set, and a style captured with one applies to
|
||||
/// text at the default, is duplicated and comes back out of a library.
|
||||
|
||||
@@ -1380,6 +1380,7 @@ font-technology Variable Променливи
|
||||
Delete Guides Изтриване на водачи
|
||||
Delete Slice Изтриване на отрязък
|
||||
Delete Style Изтриване на стил
|
||||
Delete Styles Изтриване на стилове
|
||||
Delete Variables Изтриване на променливи
|
||||
Delete View Изтриване на изглед
|
||||
Done Editing Pattern Край на редактирането на мостра
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1042,6 +1042,7 @@
|
||||
Delete Spine Point Suprimeix el punt de l’espina
|
||||
Delete Stop Suprimeix la parada
|
||||
Delete Style Suprimeix l’estil
|
||||
Delete Styles Suprimeix els estils
|
||||
Delete Swatch Suprimeix la mostra
|
||||
Delete Symbol Suprimeix el símbol
|
||||
Delete Tab Suprimeix la tabulació
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1593,6 +1593,7 @@ lighting Light Světlo
|
||||
Delete Guides Odstranit vodítka
|
||||
Delete Slice Odstranit řez
|
||||
Delete Style Odstranit styl
|
||||
Delete Styles Odstranit styly
|
||||
Delete Variables Odstranit proměnné
|
||||
Delete View Odstranit zobrazení
|
||||
Done Editing Pattern Dokončit úpravy vzorku
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5330,6 +5330,7 @@ swatch Bright Violet Kraftig violet
|
||||
Delete Guides Slet hjælpelinjer
|
||||
Delete Slice Slet udsnit
|
||||
Delete Style Slet typografi
|
||||
Delete Styles Slet typografier
|
||||
Delete Variables Slet variabler
|
||||
Delete View Slet visning
|
||||
Done Editing Pattern Redigering af mønster udført
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1406,6 +1406,7 @@ file-filter Templates Vorlagen
|
||||
Delete Guides Hilfslinien löschen
|
||||
Delete Slice Slice löschen
|
||||
Delete Style Format löschen
|
||||
Delete Styles Formate löschen
|
||||
Delete Variables Variablen löschen
|
||||
Delete View Ansicht löschen
|
||||
Done Editing Pattern Musterbearbeitung beenden
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1344,6 +1344,7 @@ font-technology Variable Μεταβλητές
|
||||
Delete Guides Διαγραφή οδηγών
|
||||
Delete Slice Διαγραφή τομής
|
||||
Delete Style Διαγραφή στυλ
|
||||
Delete Styles Διαγραφή στυλ
|
||||
Delete Variables Διαγραφή μεταβλητών
|
||||
Delete View Διαγραφή προβολής
|
||||
Done Editing Pattern Ολοκλήρωση επεξεργασίας μοτίβου
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 164 and column 5.
|
@@ -1418,6 +1418,7 @@ font-technology Variable Variables
|
||||
Delete Guides Eliminar guías
|
||||
Delete Slice Eliminar sector
|
||||
Delete Style Eliminar estilo
|
||||
Delete Styles Eliminar estilos
|
||||
Delete Variables Eliminar variables
|
||||
Delete View Eliminar vista
|
||||
Done Editing Pattern Terminar edición de motivo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1567,6 +1567,7 @@ lighting Light Valo
|
||||
Delete Guides Poista apuviivat
|
||||
Delete Slice Poista siivu
|
||||
Delete Style Poista tyyli
|
||||
Delete Styles Poista tyylit
|
||||
Delete Variables Poista muuttujat
|
||||
Delete View Poista näkymä
|
||||
Done Editing Pattern Lopeta kuvion muokkaus
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1417,6 +1417,7 @@ font-technology Variable Variables
|
||||
Delete Guides Supprimer les repères
|
||||
Delete Slice Supprimer la tranche
|
||||
Delete Style Supprimer le style
|
||||
Delete Styles Supprimer les styles
|
||||
Delete Variables Supprimer les variables
|
||||
Delete View Supprimer la vue
|
||||
Done Editing Pattern Terminer la modification du motif
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1806,6 +1806,7 @@ file-filter Templates Sablonok
|
||||
Delete Guides Segédvonalak törlése
|
||||
Delete Slice Szelet törlése
|
||||
Delete Style Stílus törlése
|
||||
Delete Styles Stílusok törlése
|
||||
Delete Variables Változók törlése
|
||||
Delete View Nézet törlése
|
||||
Done Editing Pattern Mintázatszerkesztés befejezése
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5330,6 +5330,7 @@ swatch Bright Violet Violet cerah
|
||||
Delete Guides Hapus semua panduan
|
||||
Delete Slice Hapus slice
|
||||
Delete Style Hapus gaya
|
||||
Delete Styles Hapus gaya
|
||||
Delete Variables Hapus variabel
|
||||
Delete View Hapus tampilan
|
||||
Done Editing Pattern Selesai mengedit pola
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2169 and column 5.
|
@@ -1430,6 +1430,7 @@ file-filter Templates Modelli
|
||||
Delete Guides Elimina guide
|
||||
Delete Slice Elimina sezione
|
||||
Delete Style Elimina stile
|
||||
Delete Styles Elimina stili
|
||||
Delete Variables Elimina variabili
|
||||
Delete View Elimina vista
|
||||
Done Editing Pattern Termina modifica motivo
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1713,6 +1713,7 @@ font-technology Variable バリアブル
|
||||
Delete Guides ガイドを削除
|
||||
Delete Slice スライスを削除
|
||||
Delete Style スタイルを削除
|
||||
Delete Styles スタイルを削除
|
||||
Delete Variables 変数を削除
|
||||
Delete View 表示を削除
|
||||
Done Editing Pattern パターン編集を完了
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5185,6 +5185,7 @@ imageTrace Outline კონტური
|
||||
Delete Light სინათლის წაშლა
|
||||
Delete Slice ნაჭრის წაშლა
|
||||
Delete Style სტილის წაშლა
|
||||
Delete Styles სტილების წაშლა
|
||||
Delete Variables ცვლადების წაშლა
|
||||
Delete View ხედის წაშლა
|
||||
Done Editing Pattern პატერნის რედაქტირების დასრულება
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1701,6 +1701,7 @@ font-technology Variable 가변
|
||||
Delete Guides 안내선 삭제
|
||||
Delete Slice 분할 삭제
|
||||
Delete Style 스타일 삭제
|
||||
Delete Styles 스타일 삭제
|
||||
Delete Variables 변수 삭제
|
||||
Delete View 보기 삭제
|
||||
Done Editing Pattern 패턴 편집 완료
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5341,6 +5341,7 @@ swatch Bright Violet Felviolet
|
||||
Delete Guides Hulplijnen verwijderen
|
||||
Delete Slice Slice verwijderen
|
||||
Delete Style Stijl verwijderen
|
||||
Delete Styles Stijlen verwijderen
|
||||
Delete Variables Variabelen verwijderen
|
||||
Delete View Weergave verwijderen
|
||||
Done Editing Pattern Patroon bewerken voltooid
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1508,6 +1508,7 @@ lighting Light Światło
|
||||
Delete Guides Usuń linie pomocnicze
|
||||
Delete Slice Usuń plasterek
|
||||
Delete Style Usuń styl
|
||||
Delete Styles Usuń style
|
||||
Delete Variables Usuń zmienne
|
||||
Delete View Usuń widok
|
||||
Done Editing Pattern Zakończ edycję wzorka
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1224,6 +1224,7 @@
|
||||
Delete Guides Excluir Guias
|
||||
Delete Slice Excluir Fatia
|
||||
Delete Style Excluir Estilo
|
||||
Delete Styles Excluir Estilos
|
||||
Delete Variables Excluir Variáveis
|
||||
Delete View Excluir Exibição
|
||||
Done Editing Pattern Concluir Edição de Padrão
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1428,6 +1428,7 @@ file-filter Templates Шаблоны
|
||||
Delete Guides Удалить направляющие
|
||||
Delete Slice Удалить фрагмент
|
||||
Delete Style Удалить стиль
|
||||
Delete Styles Удалить стили
|
||||
Delete Variables Удалить переменные
|
||||
Delete View Удалить вид
|
||||
Done Editing Pattern Завершить редактирование узора
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1382,6 +1382,7 @@ font-technology Variable Variabla
|
||||
Delete Guides Radera stödlinjer
|
||||
Delete Slice Radera skiva
|
||||
Delete Style Radera stil
|
||||
Delete Styles Radera stilar
|
||||
Delete Variables Radera variabler
|
||||
Delete View Radera vy
|
||||
Done Editing Pattern Avsluta mönsterredigering
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -5147,6 +5147,7 @@ theme Appearance Görünüm
|
||||
Delete Guides Kılavuzları Sil
|
||||
Delete Slice Dilimi Sil
|
||||
Delete Style Stili Sil
|
||||
Delete Styles Stilleri Sil
|
||||
Delete Variables Değişkenleri Sil
|
||||
Delete View Görünümü Sil
|
||||
Done Editing Pattern Desen Düzenlemeyi Bitir
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1424,6 +1424,7 @@ file-filter Templates Шаблони
|
||||
Delete Guides Видалити напрямні
|
||||
Delete Slice Видалити фрагмент
|
||||
Delete Style Видалити стиль
|
||||
Delete Styles Видалити стилі
|
||||
Delete Variables Видалити змінні
|
||||
Delete View Видалити вигляд
|
||||
Done Editing Pattern Завершити редагування візерунка
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 178 and column 5.
|
@@ -1637,6 +1637,7 @@ file-filter Templates Mẫu
|
||||
Delete Guides Xóa các đường căn
|
||||
Delete Slice Xóa lát cắt
|
||||
Delete Style Xóa kiểu
|
||||
Delete Styles Xóa kiểu
|
||||
Delete Variables Xóa biến số
|
||||
Delete View Xóa chế độ xem
|
||||
Done Editing Pattern Hoàn tất chỉnh sửa họa tiết
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 200 and column 5.
|
@@ -1362,6 +1362,7 @@ file-filter Templates 模板
|
||||
Delete Guides 删除参考线
|
||||
Delete Slice 删除切片
|
||||
Delete Style 删除样式
|
||||
Delete Styles 删除样式
|
||||
Delete Variables 删除变量
|
||||
Delete View 删除视图
|
||||
Done Editing Pattern 完成图案编辑
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1395,6 +1395,7 @@ file-filter Templates 範本
|
||||
Delete Guides 刪除參考線
|
||||
Delete Slice 刪除切片
|
||||
Delete Style 刪除樣式
|
||||
Delete Styles 刪除樣式
|
||||
Delete Variables 刪除變數
|
||||
Delete View 刪除檢視
|
||||
Done Editing Pattern 完成圖樣編輯
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -5,11 +5,15 @@
|
||||
//! "+" when the text overrides some of its attributes. New makes a style from the selected text;
|
||||
//! Alt-click on New first asks for its name (`ui.newCharStyle`, `ui.newParaStyle`).
|
||||
|
||||
use egui::{Sense, Ui, vec2};
|
||||
use std::cell::RefCell;
|
||||
|
||||
use egui::{Color32, Sense, Ui, vec2};
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_doc::Document;
|
||||
use vectorcraft_engine::cmd::textstyles;
|
||||
|
||||
use super::character::{end_typing, text_editing, text_style};
|
||||
use super::memo::Memo;
|
||||
use super::{alt_held, pstate, selection_len, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::theme::Tokens;
|
||||
@@ -35,28 +39,93 @@ impl Kind {
|
||||
fn key(self) -> &'static str {
|
||||
if self == Kind::Char { "char-style-sel" } else { "para-style-sel" }
|
||||
}
|
||||
/// Panel state holding the styles highlighted together, with the style [`Kind::key`] held
|
||||
/// when they were highlighted.
|
||||
fn many_key(self) -> &'static str {
|
||||
if self == Kind::Char { "char-style-many" } else { "para-style-many" }
|
||||
}
|
||||
/// Panel state holding the style a Shift-click extends the highlight from.
|
||||
fn anchor_key(self) -> &'static str {
|
||||
if self == Kind::Char { "char-style-anchor" } else { "para-style-anchor" }
|
||||
}
|
||||
/// 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" }
|
||||
}
|
||||
/// Panel state holding the `uid` of the document the highlight was made in.
|
||||
fn document_key(self) -> &'static str {
|
||||
if self == Kind::Char { "char-style-document" } else { "para-style-document" }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
/// The styles highlighted in the panel, in the order they were picked, among the styles `listed`:
|
||||
/// the styles a Shift-click, a Cmd/Ctrl-click or Select All Unused highlighted together while
|
||||
/// [`Kind::key`] still holds the style it held then, else the one style in [`Kind::key`].
|
||||
fn highlighted(app: &VectorcraftApp, ctx: &egui::Context, kind: Kind, listed: &[&str]) -> Vec<String> {
|
||||
let document = app.session.active().map(|st| st.uid);
|
||||
if pstate::<Option<u64>>(ctx, kind.document_key()) != document {
|
||||
set_pstate(ctx, kind.document_key(), document);
|
||||
set_highlighted(ctx, kind, vec![]);
|
||||
set_pstate(ctx, kind.anchor_key(), String::new());
|
||||
set_pstate::<Option<String>>(ctx, kind.scroll_key(), None);
|
||||
}
|
||||
let one: Option<String> = pstate(ctx, kind.key());
|
||||
let (with, many): (Option<String>, Vec<String>) = pstate(ctx, kind.many_key());
|
||||
let mut names = if with == one && !many.is_empty() { many } else { one.into_iter().collect() };
|
||||
names.retain(|n| listed.contains(&n.as_str()));
|
||||
names
|
||||
}
|
||||
|
||||
/// Highlights `names` in the panel: [`Kind::key`] holds the first of them.
|
||||
fn set_highlighted(ctx: &egui::Context, kind: Kind, names: Vec<String>) {
|
||||
let one = names.first().cloned();
|
||||
set_pstate(ctx, kind.key(), one.clone());
|
||||
set_pstate(ctx, kind.many_key(), (one, names));
|
||||
}
|
||||
|
||||
/// Highlights the styles the panel just made and scrolls the first one's row into view the next
|
||||
/// time it draws.
|
||||
fn highlight_made(ctx: &egui::Context, kind: Kind, names: Vec<String>) {
|
||||
set_pstate(ctx, kind.scroll_key(), names.first().cloned());
|
||||
set_highlighted(ctx, kind, names);
|
||||
}
|
||||
|
||||
/// Duplicates the styles `names` (`charStyle.duplicate`, `paraStyle.duplicate`) in one undo step
|
||||
/// and highlights the copies.
|
||||
fn duplicate(app: &mut VectorcraftApp, ctx: &egui::Context, kind: Kind, names: &[String]) {
|
||||
let p = kind.prefix();
|
||||
let made = if let [name] = names {
|
||||
app.run(&format!("{p}.duplicate"), json!({ "name": name })).map(|r| vec![r])
|
||||
} else {
|
||||
let commands: Vec<Value> = names.iter().map(|n| json!({ "command": format!("{p}.duplicate"), "params": { "name": n } })).collect();
|
||||
let label = if kind == Kind::Char { "New Character Style" } else { "New Paragraph Style" };
|
||||
app.run("command.batch", json!({ "label": label, "commands": commands })).map(|r| r["results"].as_array().cloned().unwrap_or_default())
|
||||
};
|
||||
if let Ok(made) = made {
|
||||
highlight_made(ctx, kind, made.iter().filter_map(|r| r["name"].as_str().map(str::to_string)).collect());
|
||||
}
|
||||
}
|
||||
|
||||
/// A row dragged in the list: the style's kind and name. Dropped on Create New Style it is
|
||||
/// duplicated (`charStyle.duplicate`, `paraStyle.duplicate`); dropped on Delete it is deleted
|
||||
/// (`charStyle.delete`, `paraStyle.delete`) unless it is Normal. Each panel's buttons accept only rows of
|
||||
/// their own kind.
|
||||
/// (`charStyle.delete`, `paraStyle.delete`) unless it is Normal. A highlighted row carries every
|
||||
/// highlighted style with it. Each panel's buttons accept only rows of their own kind.
|
||||
#[derive(Clone)]
|
||||
struct RowDrag {
|
||||
kind: Kind,
|
||||
name: String,
|
||||
}
|
||||
|
||||
/// The styles a row dragged from the list carries: every highlighted style (`sel`) in the order
|
||||
/// the panel lists them when the row is one of them, else the row's style alone.
|
||||
fn dragged(sel: &[String], listed: &[&str], name: &str) -> Vec<String> {
|
||||
if sel.iter().any(|s| s == name) {
|
||||
listed.iter().filter(|n| sel.iter().any(|s| s == *n)).map(|n| n.to_string()).collect()
|
||||
} else {
|
||||
vec![name.to_string()]
|
||||
}
|
||||
}
|
||||
|
||||
/// The style row of `kind` released on `button` this frame, if any; `button` is outlined while one
|
||||
/// is held over it. `take`: whether a row with that name may be dropped there.
|
||||
fn row_dropped(ui: &Ui, button: &egui::Response, kind: Kind, take: impl Fn(&str) -> bool) -> Option<String> {
|
||||
@@ -65,9 +134,18 @@ fn row_dropped(ui: &Ui, button: &egui::Response, kind: Kind, take: impl Fn(&str)
|
||||
button.dnd_release_payload::<RowDrag>().map(|_| held.name.clone())
|
||||
}
|
||||
|
||||
/// (name, attrs, uses) for every style, Normal first.
|
||||
/// (name, attrs, uses) for every style, Normal first, listed once per document version.
|
||||
fn styles(app: &mut VectorcraftApp, kind: Kind) -> Vec<(String, Map<String, Value>, u64)> {
|
||||
let v = app.session.execute(&format!("{}.list", kind.prefix()), &json!({})).unwrap_or_default();
|
||||
thread_local! {
|
||||
static MEMO: RefCell<[Memo<Document, Value>; 2]> = RefCell::new([Memo::default(), Memo::default()]);
|
||||
}
|
||||
let Some((doc, revision)) = app.session.active().map(|st| (st.doc.clone(), st.revision)) else { return vec![] };
|
||||
let list = || {
|
||||
#[cfg(test)]
|
||||
LISTED.with(|n| n.set(n.get() + 1));
|
||||
app.session.execute(&format!("{}.list", kind.prefix()), &json!({})).unwrap_or_default()
|
||||
};
|
||||
let v = MEMO.with(|m| m.borrow_mut()[kind as usize].get(&doc, revision, list));
|
||||
v["styles"]
|
||||
.as_array()
|
||||
.map(|a| {
|
||||
@@ -84,6 +162,12 @@ fn styles(app: &mut VectorcraftApp, kind: Kind) -> Vec<(String, Map<String, Valu
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// How many times [`styles`] has run `charStyle.list` or `paraStyle.list` on this thread.
|
||||
static LISTED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The selection's style name (Normal when none) and every one of its attributes, written as a
|
||||
/// style holds them, if text is selected.
|
||||
fn current(app: &VectorcraftApp, kind: Kind) -> Option<(Option<String>, Map<String, Value>)> {
|
||||
@@ -116,14 +200,33 @@ pub(crate) fn open_new(app: &mut VectorcraftApp, kind: Kind) -> Result<Value, St
|
||||
crate::dialogs::new_text_style::open(app, kind == Kind::Para, t)
|
||||
}
|
||||
|
||||
/// A style row's fill, and whether it gets an accent outline: the current style's row is filled
|
||||
/// with `row_selected` and outlined when it is also highlighted, so Cmd/Ctrl-clicking it shows.
|
||||
fn row_look(is_cur: bool, in_highlight: bool, hovered: bool, t: &Tokens) -> (Option<Color32>, bool) {
|
||||
let fill = if is_cur {
|
||||
Some(t.row_selected)
|
||||
} else if in_highlight || hovered {
|
||||
Some(t.hover)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(fill, is_cur && in_highlight)
|
||||
}
|
||||
|
||||
/// The style list and its New and Delete buttons. Shift-click highlights every style from the
|
||||
/// last one clicked without Shift through this one, and Cmd/Ctrl-click adds or removes one; both
|
||||
/// leave the text as it is. Delete deletes every highlighted style but the Normal style.
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let all = styles(app, kind);
|
||||
let cur = current(app, kind);
|
||||
let normal = all.first().map(|s| s.0.clone()).unwrap_or_default();
|
||||
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 mut clicked: Option<(String, egui::Modifiers)> = None;
|
||||
let listed: Vec<&str> = all.iter().map(|s| s.0.as_str()).collect();
|
||||
// A highlighted style that is no longer listed (renamed in Style Options, deleted by a command
|
||||
// or undone) is no longer highlighted.
|
||||
let mut sel = highlighted(app, ui.ctx(), kind, &listed);
|
||||
let scroll_to: Option<String> = pstate(ui.ctx(), kind.scroll_key());
|
||||
widgets::list_box(ui, |ui| {
|
||||
for (name, attrs, _) in &all {
|
||||
@@ -137,10 +240,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
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() {
|
||||
ui.painter().rect_filled(r, 0.0, t.hover);
|
||||
let (fill, outline) = row_look(is_cur, sel.contains(name), resp.hovered(), &t);
|
||||
if let Some(fill) = fill {
|
||||
ui.painter().rect_filled(r, 0.0, fill);
|
||||
}
|
||||
if outline {
|
||||
ui.painter().rect_stroke(r, 0.0, egui::Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
// The built-in "[Normal … Style]" (listed first) is translated where painted; other names
|
||||
// are user data, brackets or not.
|
||||
@@ -148,7 +253,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
let label = if overridden { format!("{shown}+") } else { shown.to_string() };
|
||||
ui.painter().text(r.left_center() + vec2(8.0, 0.0), egui::Align2::LEFT_CENTER, label, egui::FontId::proportional(12.5), t.text);
|
||||
if resp.clicked() {
|
||||
clicked = Some((name.clone(), ui.input(|i| i.modifiers.alt)));
|
||||
clicked = Some((name.clone(), ui.input(|i| i.modifiers)));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -156,20 +261,17 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
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());
|
||||
if has_text {
|
||||
let p = target(app, kind, json!({ "name": name, "clearOverrides": clear }));
|
||||
if let Some((name, m)) = clicked {
|
||||
sel = super::swatches::click_selection(ui, kind.anchor_key(), sel, &listed, &name, m);
|
||||
// Shift and Cmd/Ctrl clicks only highlight, as in the Graphic Styles panel.
|
||||
if has_text && !(m.shift || m.command) {
|
||||
let p = target(app, kind, json!({ "name": name, "clearOverrides": m.alt }));
|
||||
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);
|
||||
set_highlighted(ui.ctx(), kind, sel.clone());
|
||||
let doomed: Vec<&String> = sel.iter().filter(|s| **s != normal).collect();
|
||||
let deletable = !doomed.is_empty();
|
||||
widgets::bottom_bar(ui, |ui| {
|
||||
ui.add_space((ui.available_width() - 2.0 * 28.0).max(0.0));
|
||||
let new = widgets::icon_button_enabled(
|
||||
@@ -181,15 +283,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
24.0,
|
||||
);
|
||||
if let Some(name) = row_dropped(ui, &new, kind, |_| true) {
|
||||
if let Ok(r) = app.run(&format!("{}.duplicate", kind.prefix()), json!({ "name": name })) {
|
||||
highlight_made(ui.ctx(), kind, r["name"].as_str().map(str::to_string));
|
||||
}
|
||||
duplicate(app, ui.ctx(), kind, &dragged(&sel, &listed, &name));
|
||||
} else if new.clicked() {
|
||||
end_typing(app);
|
||||
if alt_held(ui) {
|
||||
app.run(if kind == Kind::Char { "ui.newCharStyle" } else { "ui.newParaStyle" }, json!({})).ok();
|
||||
} else if let Ok(r) = app.run(&format!("{}.new", kind.prefix()), target(app, kind, json!({}))) {
|
||||
highlight_made(ui.ctx(), kind, r["name"].as_str().map(str::to_string));
|
||||
highlight_made(ui.ctx(), kind, r["name"].as_str().map(str::to_string).into_iter().collect());
|
||||
}
|
||||
}
|
||||
let delete = widgets::icon_button_enabled(
|
||||
@@ -200,23 +300,31 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
deletable,
|
||||
24.0,
|
||||
);
|
||||
if let Some(name) = row_dropped(ui, &delete, kind, |n| n != normal) {
|
||||
if app.run(&format!("{}.delete", kind.prefix()), json!({ "name": name })).is_ok() && sel.as_ref() == Some(&name) {
|
||||
set_pstate::<Option<String>>(ui.ctx(), kind.key(), None);
|
||||
let drop_names = |n: &str| dragged(&sel, &listed, n).into_iter().filter(|s| *s != normal).collect::<Vec<_>>();
|
||||
if let Some(name) = row_dropped(ui, &delete, kind, |n| !drop_names(n).is_empty()) {
|
||||
// Deleting the highlighted styles clears the highlight; a row dropped from outside it
|
||||
// leaves it as it is.
|
||||
if app.run(&format!("{}.delete", kind.prefix()), json!({ "names": drop_names(&name) })).is_ok() && sel.contains(&name) {
|
||||
set_highlighted(ui.ctx(), kind, vec![]);
|
||||
}
|
||||
} else if delete.clicked()
|
||||
&& let Some(n) = &sel
|
||||
&& app.run(&format!("{}.delete", kind.prefix()), json!({ "name": n })).is_ok()
|
||||
{
|
||||
set_pstate::<Option<String>>(ui.ctx(), kind.key(), None);
|
||||
} else if delete.clicked() && app.run(&format!("{}.delete", kind.prefix()), json!({ "names": doomed })).is_ok() {
|
||||
set_highlighted(ui.ctx(), kind, vec![]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The panel menu. Select All Unused highlights the styles no text uses (`charStyle.unused`,
|
||||
/// `paraStyle.unused`), Duplicate copies every highlighted style and highlights the copies, and
|
||||
/// Delete deletes every highlighted style but the Normal style.
|
||||
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
let sel: Option<String> = pstate(ui.ctx(), kind.key());
|
||||
let all = styles(app, kind);
|
||||
let listed: Vec<&str> = all.iter().map(|s| s.0.as_str()).collect();
|
||||
let many = highlighted(app, ui.ctx(), kind, &listed);
|
||||
// The items for one style act on the highlighted style while it is the only one.
|
||||
let sel: Option<String> = if many.len() == 1 { many.first().cloned() } else { None };
|
||||
let normal = if kind == Kind::Char { "[Normal Character Style]" } else { "[Normal Paragraph Style]" };
|
||||
let user = sel.as_ref().is_some_and(|s| s != normal);
|
||||
let doomed: Vec<&String> = many.iter().filter(|s| *s != normal).collect();
|
||||
let has_text = current(app, kind).is_some() || selection_len(app) > 0;
|
||||
let p = kind.prefix();
|
||||
if menu_item(ui, &crate::i18n::fmt(tl!("New {kind} Style…"), &[("kind", kind.what_label())]), true, false) {
|
||||
@@ -224,17 +332,19 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) {
|
||||
end_typing(app);
|
||||
app.run(&format!("{p}.new"), target(app, kind, json!({}))).ok();
|
||||
}
|
||||
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 }))
|
||||
{
|
||||
highlight_made(ui.ctx(), kind, r["name"].as_str().map(str::to_string));
|
||||
if menu_item(ui, &crate::i18n::fmt(tl!("Duplicate {kind} Style"), &[("kind", kind.what_label())]), !many.is_empty(), false) {
|
||||
let names: Vec<String> = listed.iter().filter(|n| many.iter().any(|s| s == *n)).map(|n| n.to_string()).collect();
|
||||
duplicate(app, ui.ctx(), kind, &names);
|
||||
}
|
||||
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()
|
||||
if menu_item(ui, &crate::i18n::fmt(tl!("Delete {kind} Style"), &[("kind", kind.what_label())]), !doomed.is_empty(), false)
|
||||
&& app.run(&format!("{p}.delete"), json!({ "names": doomed })).is_ok()
|
||||
{
|
||||
set_pstate::<Option<String>>(ui.ctx(), kind.key(), None);
|
||||
set_highlighted(ui.ctx(), kind, vec![]);
|
||||
}
|
||||
if menu_item(ui, tl!("Select All Unused"), app.session.active().is_some(), false)
|
||||
&& let Ok(r) = app.run(&format!("{p}.unused"), json!({}))
|
||||
{
|
||||
set_highlighted(ui.ctx(), kind, serde_json::from_value(r["names"].clone()).unwrap_or_default());
|
||||
}
|
||||
ui.separator();
|
||||
if menu_item(ui, tl!("Clear Overrides"), sel.is_some() && has_text, false)
|
||||
|
||||
@@ -3,6 +3,16 @@ use vectorcraft_doc::{NodeId, NodeKind};
|
||||
|
||||
use super::super::tests_appearance::{click, frame_events, frame_raw, text_rect};
|
||||
|
||||
#[test]
|
||||
fn current_style_highlight_has_a_separate_indicator() {
|
||||
let ctx = egui::Context::default();
|
||||
let t = Tokens::get(&ctx);
|
||||
assert_eq!(row_look(true, false, false, &t), (Some(t.row_selected), false));
|
||||
assert_eq!(row_look(true, true, false, &t), (Some(t.row_selected), true));
|
||||
assert_eq!(row_look(false, true, false, &t), (Some(t.hover), false));
|
||||
assert_eq!(row_look(false, false, false, &t), (None, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panels_draw_and_apply_headless() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
@@ -309,7 +319,7 @@ fn a_style_made_from_the_panel_is_scrolled_into_view() {
|
||||
} 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()));
|
||||
highlight_made(&ctx, kind, vec![name.clone()]);
|
||||
name
|
||||
};
|
||||
assert_eq!(pstate::<Option<String>>(&ctx, kind.key()).as_deref(), Some(made.as_str()), "{what}: the new style is highlighted");
|
||||
@@ -493,6 +503,243 @@ fn character_style_range_apply_from_panel_control() {
|
||||
assert_eq!(app.session.doc().unwrap().doc, after);
|
||||
}
|
||||
|
||||
/// A document with selected type and both kinds' styles A, B, C and D, the type using B.
|
||||
fn several_app() -> (VectorcraftApp, NodeId) {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
let id = NodeId(app.run("text.create", json!({"x": 20, "y": 50, "text": "Hi", "size": 18})).unwrap()["id"].as_u64().unwrap());
|
||||
app.run("select.set", json!({"ids": [id.0]})).unwrap();
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
for n in ["A", "B", "C", "D"] {
|
||||
app.run(&format!("{}.new", kind.prefix()), json!({"name": n, "attrs": {}})).unwrap();
|
||||
}
|
||||
app.run(&format!("{}.apply", kind.prefix()), json!({"name": "B"})).unwrap();
|
||||
}
|
||||
(app, id)
|
||||
}
|
||||
|
||||
/// Click the text `label` that `draw` paints, with `modifiers` held.
|
||||
fn click_text_held(
|
||||
ctx: &egui::Context,
|
||||
app: &mut VectorcraftApp,
|
||||
label: &str,
|
||||
modifiers: egui::Modifiers,
|
||||
draw: impl FnMut(&mut VectorcraftApp, &mut egui::Ui),
|
||||
) {
|
||||
click_where(ctx, app, |texts| text_rect(texts, label).center(), modifiers, draw);
|
||||
}
|
||||
|
||||
/// Click where `at` finds in the texts `draw` paints, with `modifiers` held, over three frames
|
||||
/// (hover, press, release) after two frames that lay them out.
|
||||
fn click_where(
|
||||
ctx: &egui::Context,
|
||||
app: &mut VectorcraftApp,
|
||||
at: impl FnOnce(&[(String, egui::Rect)]) -> egui::Pos2,
|
||||
modifiers: egui::Modifiers,
|
||||
mut draw: impl FnMut(&mut VectorcraftApp, &mut egui::Ui),
|
||||
) {
|
||||
let held = egui::Event::ModifiersChanged(modifiers);
|
||||
frame_events(ctx, app, vec![held.clone()], &mut draw);
|
||||
let pos = at(&frame_events(ctx, app, vec![held.clone()], &mut draw));
|
||||
let button = |pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers };
|
||||
for e in [egui::Event::PointerMoved(pos), button(true), button(false)] {
|
||||
frame_events(ctx, app, vec![held.clone(), e], &mut draw);
|
||||
}
|
||||
frame_events(ctx, app, vec![egui::Event::ModifiersChanged(egui::Modifiers::NONE)], &mut draw);
|
||||
}
|
||||
|
||||
/// The styles `kind`'s panel highlights.
|
||||
fn highlighted_now(app: &mut VectorcraftApp, ctx: &egui::Context, kind: Kind) -> Vec<String> {
|
||||
let all = styles(app, kind);
|
||||
highlighted(app, ctx, kind, &all.iter().map(|s| s.0.as_str()).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn switching_documents_clears_highlighted_styles_with_matching_names() {
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
let (mut app, _) = several_app();
|
||||
let ctx = egui::Context::default();
|
||||
let rows = |app: &mut VectorcraftApp, ui: &mut egui::Ui| {
|
||||
ui.set_width(280.0);
|
||||
show(app, ui, kind);
|
||||
};
|
||||
click_text_held(&ctx, &mut app, "A", egui::Modifiers::NONE, rows);
|
||||
click_text_held(&ctx, &mut app, "B", egui::Modifiers::COMMAND, rows);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A", "B"]);
|
||||
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
for name in ["A", "B"] {
|
||||
app.run(&format!("{}.new", kind.prefix()), json!({"name": name, "attrs": {}})).unwrap();
|
||||
}
|
||||
assert!(highlighted_now(&mut app, &ctx, kind).is_empty(), "{} Styles: the new document has no highlighted styles", kind.what());
|
||||
assert_eq!(pstate::<String>(&ctx, kind.anchor_key()), "");
|
||||
assert_eq!(pstate::<Option<String>>(&ctx, kind.key()), None);
|
||||
assert_eq!(pstate::<(Option<String>, Vec<String>)>(&ctx, kind.many_key()).1, Vec::<String>::new());
|
||||
}
|
||||
}
|
||||
|
||||
/// In both panels Shift-click highlights every style from the last one clicked without Shift
|
||||
/// through the one clicked, and Cmd/Ctrl-click adds or removes one; neither applies a style to the
|
||||
/// selected type.
|
||||
/// Delete then deletes every highlighted style in one undo step, and the type keeps its look.
|
||||
#[test]
|
||||
fn shift_and_cmd_clicks_highlight_several_styles_that_delete_deletes_at_once() {
|
||||
use egui::Modifiers;
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
let (mut app, id) = several_app();
|
||||
let ctx = egui::Context::default();
|
||||
let rows = |app: &mut VectorcraftApp, ui: &mut egui::Ui| {
|
||||
ui.set_width(280.0);
|
||||
show(app, ui, kind);
|
||||
};
|
||||
click_text_held(&ctx, &mut app, "A", Modifiers::NONE, rows);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A"]);
|
||||
let doc = app.session.doc().unwrap().doc.clone();
|
||||
click_text_held(&ctx, &mut app, "C", Modifiers::SHIFT, rows);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A", "B", "C"], "{} Styles: Shift-click", kind.what());
|
||||
click_text_held(&ctx, &mut app, "B", Modifiers::COMMAND, rows);
|
||||
click_text_held(&ctx, &mut app, "D", Modifiers::COMMAND, rows);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A", "C", "D"], "{} Styles: Cmd/Ctrl-click", kind.what());
|
||||
assert_eq!(app.session.doc().unwrap().doc, doc, "Shift and Cmd/Ctrl clicks apply nothing");
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
// The footer's Delete button is centered 12 points left of the panel's right edge.
|
||||
let button = std::cell::Cell::new(egui::Pos2::ZERO);
|
||||
let footer = |app: &mut VectorcraftApp, ui: &mut egui::Ui| {
|
||||
ui.set_width(280.0);
|
||||
let right = ui.max_rect().right();
|
||||
show(app, ui, kind);
|
||||
button.set(egui::pos2(right - 12.0, ui.min_rect().bottom() - 12.0));
|
||||
};
|
||||
click_where(&ctx, &mut app, |_| button.get(), Modifiers::NONE, footer);
|
||||
let left: Vec<String> = styles(&mut app, kind).into_iter().skip(1).map(|s| s.0).collect();
|
||||
assert_eq!(left, ["B"], "{} Styles: Delete deletes A, C and D", kind.what());
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), Vec::<String>::new());
|
||||
assert_eq!(text_runs(&app, id)[0].1, 18.0, "the type keeps its look");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(app.session.doc().unwrap().doc, doc);
|
||||
}
|
||||
}
|
||||
|
||||
/// The panel menu's Select All Unused highlights, in both panels, the styles no text uses, never
|
||||
/// the Normal style; the menu's Delete then deletes them in one undo step.
|
||||
#[test]
|
||||
fn select_all_unused_highlights_the_unused_styles_for_delete() {
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
let (mut app, _) = several_app();
|
||||
let ctx = egui::Context::default();
|
||||
let items = |app: &mut VectorcraftApp, ui: &mut egui::Ui| menu(app, ui, kind);
|
||||
click_text_held(&ctx, &mut app, " Select All Unused", egui::Modifiers::NONE, items);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A", "C", "D"], "{} Styles", kind.what());
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
let label = format!(" Delete {} Style", kind.what());
|
||||
click_text_held(&ctx, &mut app, &label, egui::Modifiers::NONE, items);
|
||||
let left: Vec<String> = styles(&mut app, kind).into_iter().map(|s| s.0).collect();
|
||||
assert_eq!(left[1..], ["B"], "{} Styles: the unused styles are deleted", kind.what());
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), Vec::<String>::new());
|
||||
// With every style used, nothing is highlighted.
|
||||
click_text_held(&ctx, &mut app, " Select All Unused", egui::Modifiers::NONE, items);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), Vec::<String>::new());
|
||||
}
|
||||
}
|
||||
|
||||
/// With several styles highlighted, the panel menu's Duplicate copies each of them in one undo
|
||||
/// step, highlights the copies and scrolls the first copy's row into view.
|
||||
#[test]
|
||||
fn panel_menu_duplicate_copies_every_highlighted_style() {
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
let what = kind.what();
|
||||
let (mut app, _) = several_app();
|
||||
let ctx = egui::Context::default();
|
||||
let items = |app: &mut VectorcraftApp, ui: &mut egui::Ui| menu(app, ui, kind);
|
||||
click_text_held(&ctx, &mut app, " Select All Unused", egui::Modifiers::NONE, items);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A", "C", "D"], "{what} Styles");
|
||||
let before = styles(&mut app, kind);
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
click_text_held(&ctx, &mut app, &format!(" Duplicate {what} Style"), egui::Modifiers::NONE, items);
|
||||
let names: Vec<String> = styles(&mut app, kind).into_iter().skip(1).map(|s| s.0).collect();
|
||||
assert_eq!(names, ["A", "B", "C", "D", "A copy", "C copy", "D copy"], "{what} Styles: A, C and D are duplicated");
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "{what} Styles: one undo step");
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A copy", "C copy", "D copy"], "{what} Styles: the copies are highlighted");
|
||||
assert_eq!(pstate::<Option<String>>(&ctx, kind.scroll_key()).as_deref(), Some("A copy"), "{what} Styles: the first copy scrolls into view");
|
||||
app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(styles(&mut app, kind), before);
|
||||
}
|
||||
}
|
||||
|
||||
/// The panel and its menu list the styles, and count their uses, once per document version: more
|
||||
/// frames of an unchanged document list nothing again, and an edit lists them again.
|
||||
#[test]
|
||||
fn the_styles_are_listed_once_per_document_version() {
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
let what = kind.what();
|
||||
let (mut app, _) = several_app();
|
||||
let ctx = egui::Context::default();
|
||||
let draw = |app: &mut VectorcraftApp, ui: &mut egui::Ui| {
|
||||
show(app, ui, kind);
|
||||
menu(app, ui, kind);
|
||||
};
|
||||
let listed = || LISTED.with(|n| n.get());
|
||||
frame_events(&ctx, &mut app, vec![], draw);
|
||||
let first = listed();
|
||||
for _ in 0..4 {
|
||||
frame_events(&ctx, &mut app, vec![], draw);
|
||||
}
|
||||
assert_eq!(listed(), first, "{what} Styles: unchanged frames list nothing again");
|
||||
app.run(&format!("{}.new", kind.prefix()), json!({"name": "E", "attrs": {}})).unwrap();
|
||||
frame_events(&ctx, &mut app, vec![], draw);
|
||||
assert_eq!(listed(), first + 1, "{what} Styles: an edit lists the styles again");
|
||||
assert_eq!(styles(&mut app, kind).last().map(|s| s.0.as_str()), Some("E"), "{what} Styles: the new style is listed");
|
||||
}
|
||||
}
|
||||
|
||||
/// A row dragged from the highlighted styles carries all of them: dropped on Create New Style it
|
||||
/// duplicates each in one undo step and highlights the copies, and dropped on Delete it deletes each
|
||||
/// but the Normal style in one undo step and clears the highlight. A row from outside the highlight
|
||||
/// carries only its own style; dropped on Delete it leaves the highlight as it is.
|
||||
#[test]
|
||||
fn dragging_a_highlighted_style_carries_every_highlighted_style() {
|
||||
use egui::Modifiers;
|
||||
for kind in [Kind::Char, Kind::Para] {
|
||||
let what = kind.what();
|
||||
let (mut app, _) = several_app();
|
||||
let ctx = egui::Context::default();
|
||||
let rows = |app: &mut VectorcraftApp, ui: &mut egui::Ui| {
|
||||
ui.set_width(280.0);
|
||||
show(app, ui, kind);
|
||||
};
|
||||
let names = |app: &mut VectorcraftApp| styles(app, kind).into_iter().skip(1).map(|s| s.0).collect::<Vec<_>>();
|
||||
let undo_len = |app: &VectorcraftApp| app.session.doc().unwrap().history.undo.len();
|
||||
click_text_held(&ctx, &mut app, "A", Modifiers::NONE, rows);
|
||||
click_text_held(&ctx, &mut app, "C", Modifiers::COMMAND, rows);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A", "C"], "{what} Styles");
|
||||
|
||||
let undo = undo_len(&app);
|
||||
assert!(drag_row_onto_footer(&ctx, &mut app, kind, "C", 40.0, false), "{what} Styles: New is outlined");
|
||||
assert_eq!(names(&mut app), ["A", "B", "C", "D", "A copy", "C copy"], "{what} Styles: A and C are duplicated");
|
||||
assert_eq!(undo_len(&app), undo + 1, "{what} Styles: one undo step for the copies");
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["A copy", "C copy"], "{what} Styles: the copies are highlighted");
|
||||
assert!(drag_row_onto_footer(&ctx, &mut app, kind, "B", 40.0, false), "{what} Styles: New is outlined");
|
||||
assert_eq!(names(&mut app), ["A", "B", "C", "D", "A copy", "C copy", "B copy"], "{what} Styles: only B is duplicated");
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), ["B copy"], "{what} Styles: the copy of B is highlighted");
|
||||
|
||||
let normal = super::super::label_or_name(&styles(&mut app, kind)[0].0, true).to_string();
|
||||
click_text_held(&ctx, &mut app, "A copy", Modifiers::NONE, rows);
|
||||
click_text_held(&ctx, &mut app, "C copy", Modifiers::COMMAND, rows);
|
||||
click_text_held(&ctx, &mut app, &normal, Modifiers::COMMAND, rows);
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind).len(), 3, "{what} Styles: A copy, C copy and Normal are highlighted");
|
||||
assert!(drag_row_onto_footer(&ctx, &mut app, kind, "D", 12.0, false), "{what} Styles: Delete is outlined");
|
||||
assert_eq!(names(&mut app), ["A", "B", "C", "A copy", "C copy", "B copy"], "{what} Styles: only D is deleted");
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind).len(), 3, "{what} Styles: the highlight stays");
|
||||
let undo = undo_len(&app);
|
||||
assert!(drag_row_onto_footer(&ctx, &mut app, kind, &normal, 12.0, false), "{what} Styles: Delete is outlined for the highlight");
|
||||
assert_eq!(names(&mut app), ["A", "B", "C", "B copy"], "{what} Styles: A copy and C copy are deleted, Normal stays");
|
||||
assert_eq!(undo_len(&app), undo + 1, "{what} Styles: one undo step for the deletes");
|
||||
assert_eq!(highlighted_now(&mut app, &ctx, kind), Vec::<String>::new(), "{what} Styles: the highlight is cleared");
|
||||
}
|
||||
}
|
||||
|
||||
/// Feed real text/shortcut events through the canvas Type routing. No focus reset or typing
|
||||
/// settlement here: the capture dispatch itself must close the live typing interaction.
|
||||
fn capture_input(ctx: &egui::Context, app: &mut VectorcraftApp, events: Vec<egui::Event>) {
|
||||
|
||||
+5
-1
@@ -5020,7 +5020,11 @@ spaceBefore, spaceAfter, hyphenate, mojikumi, burasagari, direction, leadingMode
|
||||
touches (a caret, `start == end`, touches its paragraph). Without a range they change every paragraph of the
|
||||
targeted objects, as before. With a range, the character attributes of `text.setStyle` and `text.setFormat` style
|
||||
that range of characters. `text.tabs.get {start?}` and `paraStyle.new` / `paraStyle.redefine {id, start?}` read the
|
||||
paragraph at `start`; `paraStyle.list` counts paragraphs as uses. A Return the Type tool types (or `text.editRange`
|
||||
paragraph at `start`; `paraStyle.list` counts paragraphs as uses, and `charStyle.list` counts runs; both count the type in
|
||||
the layers, opacity masks, symbols, graph designs, pattern tiles and the art of brushes. `charStyle.unused` and
|
||||
`paraStyle.unused` (Select All Unused in the Character Styles and Paragraph Styles panel menus) list the styles with no
|
||||
uses, in panel order and never the Normal style, and the panel highlights them; `charStyle.delete` and
|
||||
`paraStyle.delete` take `{names}` to delete several styles in one undo step. A Return the Type tool types (or `text.editRange`
|
||||
inserting `\n`) continues the style of the paragraph it splits; deleting a break keeps the first paragraph's style;
|
||||
`text.setText` gives every paragraph the first one's attributes. Threaded text keeps each paragraph's attributes as
|
||||
the story re-flows (a paragraph split between frames has them in both).
|
||||
|
||||
@@ -948,6 +948,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Type inside a group, layer, symbol instance, blend or repeat keeps its opacity, blend mode, opacity mask, Opacity & Mask Define Knockout Shape, own effects, perspective and visibility when the effects on the container outline it. Hidden type with a geometry effect of its own is left out of exports.
|
||||
- **Graphic styles:** styles keep opacity and blend mode, capture groups and type, apply on top with Alt, and link to the objects using them; Redefine, Break Link, Graphic Style Options, Select All Unused, Sort by Name, and Select > Same Graphic Style / Appearance Attribute. The panel has list and thumbnail views with rendered previews, merge, reorder and drag and drop, plus a Control bar Style picker; six generated style libraries save and load as `.vcstyles`.
|
||||
In the Graphic Styles panel, Shift-click selects a range of styles and Ctrl/Cmd-click adds or removes one.
|
||||
- **Several text styles at once:** in the Character Styles and Paragraph Styles panels, Shift-click highlights a range of styles and Ctrl/Cmd-click adds or removes one, without applying them; the panel menu's Select All Unused (`charStyle.unused`, `paraStyle.unused`) highlights the styles no type uses (in the layers, opacity masks, symbols, graph designs, pattern tiles and the art of brushes), never the Normal style; Delete, the button or the menu item, deletes every highlighted style but Normal in one undo step (`charStyle.delete {names}`, `paraStyle.delete {names}`). The menu's Duplicate copies every highlighted style in one undo step and highlights the copies. A highlighted row dragged onto Create New Style or Delete carries every highlighted style: dropped on Create New Style it copies them all and highlights the copies, and dropped on Delete it deletes them all but Normal and clears the highlight; a row from outside the highlight carries only its own style. Public reference: [Character and paragraph styles](https://web.archive.org/web/20261015081534/https://helpx.adobe.com/illustrator/using/character-paragraph-styles.html).
|
||||
- **Neutral wording:** labels, MCP tool text, docs and packaging use neutral names, and `cargo xtask brands` (part of `cargo xtask ci`) fails on vendor names.
|
||||
- **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize.
|
||||
The Libraries panel holds gradients and patterns: its + button adds the selection's fill or stroke gradient or pattern, a gradient or pattern swatch dragged from the Swatches panel is added, and a click paints the selection through the active Fill/Stroke proxy, a pattern bringing its definition and swatch into the document.
|
||||
|
||||
Reference in New Issue
Block a user