From 7907cde2709469ff37b025c4c7c1bb1e9f0e3938 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Sat, 24 Oct 2026 14:01:28 +0200 Subject: [PATCH] Swatches: a row's accessible name says what its marks show Screen readers got only the swatch name; the painted marks (colour type and mode, can't be edited) were not in it. The row is now named "Cyan, Process, CMYK" or "[Black], Process, CMYK, Cannot be edited" (translated; one new string). Tests find rows by name with names_swatch. Found by CodeRabbit. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/dialogs/options.rs | 3 +- crates/ui-egui/src/i18n.rs | 1 + crates/ui-egui/src/i18n/ar.rs | 1 + crates/ui-egui/src/i18n/cs.rs | 1 + crates/ui-egui/src/i18n/de.rs | 1 + crates/ui-egui/src/i18n/it.rs | 1 + crates/ui-egui/src/i18n/ja.rs | 1 + crates/ui-egui/src/i18n/ka.rs | 1 + crates/ui-egui/src/i18n/pl.rs | 1 + crates/ui-egui/src/i18n/pt_br.rs | 1 + crates/ui-egui/src/i18n/ru.rs | 1 + crates/ui-egui/src/i18n/uk.rs | 1 + crates/ui-egui/src/panels/swatches.rs | 62 ++++++++++++++++++++++++--- 13 files changed, 68 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/dialogs/options.rs b/crates/ui-egui/src/dialogs/options.rs index d8aba2bea..df7dc4cf0 100644 --- a/crates/ui-egui/src/dialogs/options.rs +++ b/crates/ui-egui/src/dialogs/options.rs @@ -342,7 +342,8 @@ mod tests { } fn double_click(h: &mut Harness<'static, DesignApp>, label: &str) { - let at = h.get_by_label(label).rect().center(); + // A swatch row's accessible name is its name, then what its marks show. + let at = h.get_by(|n| n.label().is_some_and(|l| crate::panels::swatches::names_swatch(&l, label))).rect().center(); h.hover_at(at); // Two clicks without the pointer leaving (`drop_at` would end the double click). for pressed in [true, false, true, false] { diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index bdca83589..158262da4 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -1010,6 +1010,7 @@ const TABLE: &[(&str, [&str; 5])] = &[ ], ), ("Load Styles", ["Formate laden", "Charger les styles", "Cargar estilos", "スタイルを読み込み", "加载样式"]), + ("Cannot be edited", ["Nicht bearbeitbar", "Non modifiable", "No editable", "編集不可", "不可编辑"]), ( "Differs from this document", ["Weicht von diesem Dokument ab", "Diffère de ce document", "Difiere de este documento", "このドキュメントと異なります", "与此文档不同"], diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 31f7fb70d..fa50476a3 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Cannot be edited", "غير قابل للتحرير"), ("Paste Vector Art", "لصق رسم متجه"), ("Baseline Options", "خيارات خط الأساس"), ("Fixed Number", "عدد ثابت"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index 4d41c30dc..e28fd4f27 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("Cannot be edited", "Nelze upravit"), ("Reverse Path", "Obrátit cestu"), ("Set Anchor", "Nastavit kotvu"), ("Clear", "Vymazat"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index 367ca52f8..63f7b6172 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Ausschneiden"), ("Copy", "Kopieren"), ("Paste", "Einfügen"), + ("Cannot be edited", "Nicht bearbeitbar"), ("Inches Decimal", "Zoll dezimal"), ("Back", "Zurück"), ("Warning", "Warnung"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 755272fe5..7de7b7ee4 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Cannot be edited", "Non modificabile"), ("Paste Vector Art", "Incolla grafica vettoriale"), ("Baseline Options", "Opzioni linea di base"), ("Fixed Number", "Numero fisso"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 37b14ac5c..601801055 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -30,6 +30,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "カット"), ("Copy", "コピー"), ("Paste", "ペースト"), + ("Cannot be edited", "編集不可"), ("Paste Vector Art", "ベクトルアートをペースト"), ("Baseline Options", "ベースラインオプション"), ("Column Rules", "段落境界線"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index 0300b62d7..1806a43e0 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("Cannot be edited", "რედაქტირება შეუძლებელია"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), ("Warning", "გაფრთხილება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 7d1d12c67..7c131b088 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("Cannot be edited", "Nie można edytować"), ("Clear", "Wyczyść"), ("Duplicate", "Powiel"), ("Select All", "Zaznacz wszystko"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index f552e9a63..f9ff794e9 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Cannot be edited", "Não editável"), ("Paste Vector Art", "Colar arte vetorial"), ("Baseline Options", "Opções de linha de base"), ("Fixed Number", "Número fixo"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 3f6ef1be0..16c4aba10 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), + ("Cannot be edited", "Нельзя изменить"), ("Clear", "Очистить"), ("Duplicate", "Дублировать"), ("Select All", "Выделить все"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index 9c7cc855a..c9f2b1abb 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Cannot be edited", "Не можна змінити"), ("Paste Vector Art", "Вставити векторну графіку"), ("Baseline Options", "Параметри базової лінії"), ("Fixed Number", "Фіксована кількість"), diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 71b7f0ed1..fc87a4d6a 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -281,6 +281,34 @@ fn row_marks(sw: &designcraft_color::swatch::Swatch) -> RowMarks { } } +/// A row's accessible name: the swatch's name, then what its marks show (colour type, colour +/// mode, that it can't be edited). [None] and [Registration] say their mode by name. +fn row_label(lang: &str, name: &str, marks: RowMarks) -> String { + let tr = |s: &str| crate::i18n::tr(lang, s).to_string(); + let mut parts = vec![name.to_string()]; + match marks.kind { + Some(ColorType::Process) => parts.push(tr("Process")), + Some(ColorType::Spot) => parts.push(tr("Spot")), + None => {} + } + match marks.mode { + Some(RowMode::Cmyk) => parts.push(tr("CMYK")), + Some(RowMode::Rgb) => parts.push(tr("RGB")), + Some(RowMode::Gray) => parts.push(tr("Gray")), + Some(RowMode::None | RowMode::Registration) | None => {} + } + if marks.not_editable { + parts.push(tr("Cannot be edited")); + } + parts.join(", ") +} + +/// Whether accessible name `label` is swatch `name`'s row (its name, then its marks). +#[cfg(test)] +pub(crate) fn names_swatch(label: &str, name: &str) -> bool { + label.strip_prefix(name).is_some_and(|rest| rest.is_empty() || rest.starts_with(", ")) +} + /// A pencil with a stroke through it: the swatch can't be edited. Drawn in code. fn paint_not_editable(p: &egui::Painter, r: egui::Rect, ink: egui::Color32) { // The pencil: its tip at the lower left, its body up to the right. @@ -379,7 +407,10 @@ fn swatch_row( let t = Tokens::get(ui.ctx()); // Rows can be dragged onto the Gradient panel's ramp. let (row, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 22.0), Sense::click_and_drag()); - resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &sw.name)); + // What the row's marks show, for screen readers too: "Cyan, Process, CMYK". + let marks = row_marks(sw); + let spoken = row_label(&app.ui.language, &sw.name, marks); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &spoken)); resp.dnd_set_drag_payload(SwatchDrag(sw.name.clone())); if resp.dragged() && let Some(p) = ui.ctx().pointer_interact_pos() @@ -401,7 +432,6 @@ fn swatch_row( crate::widgets::paint_chip(ui.painter(), chip, c, g); ui.painter().text(row.min + vec2(28.0 + indent, 11.0), egui::Align2::LEFT_CENTER, &sw.name, egui::FontId::proportional(12.5), t.text); // InDesign's columns on the right: not editable, colour type, colour mode. - let marks = row_marks(sw); let col = |k: f32| egui::Rect::from_min_size(egui::pos2(row.max.x - 19.0 - 18.0 * k, row.min.y + 4.5), vec2(13.0, 13.0)); if marks.not_editable { paint_not_editable(ui.painter(), col(2.0), t.text_dim); @@ -1205,7 +1235,7 @@ mod gradient_panel_tests { // The thumbnail sits left of the Type row. let ty = h.get_by_label("Type:").rect(); let from = egui::pos2(ty.min.x - 21.0, ty.min.y + 12.0); - let to = h.get_by_label(label).rect().center(); + let to = h.get_by(|n| n.label().is_some_and(|l| crate::panels::swatches::names_swatch(&l, label))).rect().center(); h.event(Event::PointerMoved(from)); h.step(); h.event(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); @@ -1317,14 +1347,14 @@ mod swatches_header_tests { let mut h = harness(app); h.run_steps(3); // Fill in front: a swatch is the fill. - h.get_by_label("C=0 M=100 Y=0 K=0").click(); + h.get_by(|n| n.label().is_some_and(|l| crate::panels::swatches::names_swatch(&l, "C=0 M=100 Y=0 K=0"))).click(); h.run_steps(2); assert_eq!(items(h.state())[0].fill.swatch, "C=0 M=100 Y=0 K=0"); // The proxy brings the stroke to the front; the next swatch is the stroke. h.get_by_label("Fill").click(); h.run_steps(2); assert!(h.state().ui.stroke_front); - h.get_by_label("C=100 M=0 Y=0 K=0").click(); + h.get_by(|n| n.label().is_some_and(|l| crate::panels::swatches::names_swatch(&l, "C=100 M=0 Y=0 K=0"))).click(); h.run_steps(2); let it = &items(h.state())[0]; assert_eq!((it.fill.swatch.as_str(), it.stroke.swatch.as_str()), ("C=0 M=100 Y=0 K=0", "C=100 M=0 Y=0 K=0")); @@ -1336,7 +1366,7 @@ mod swatches_header_tests { h.run_steps(2); assert!(!h.state().ui.formatting_affects_text); // Delete Swatch removes the swatch last clicked. - h.get_by_label("Brand").click(); + h.get_by(|n| n.label().is_some_and(|l| crate::panels::swatches::names_swatch(&l, "Brand"))).click(); h.run_steps(2); h.get_by_label("Delete Swatch").click(); h.run_steps(2); @@ -1375,7 +1405,25 @@ mod row_mark_tests { use designcraft_color::swatch::{Swatch, default_swatches}; use designcraft_color::{Color, ColorType, SwatchValue}; - use super::{RowMarks, RowMode, row_marks}; + use super::{RowMarks, RowMode, row_label, row_marks}; + + /// A row's accessible name says what its marks show. + #[test] + fn swatch_rows_say_their_marks() { + let all = default_swatches(); + let label = |name: &str| row_label("", name, row_marks(all.iter().find(|s| s.name == name).unwrap())); + assert_eq!(label("[Black]"), "[Black], Process, CMYK, Cannot be edited"); + assert_eq!(label("[None]"), "[None], Cannot be edited"); + assert_eq!(label("[Paper]"), "[Paper]"); + let cyan = all.iter().find(|s| matches!(&s.value, SwatchValue::Color { .. }) && !s.locked).unwrap(); + let spot = Swatch { + name: "Pantone".into(), + value: SwatchValue::Color { color: Color::Rgb { r: 1.0, g: 0.0, b: 0.0 }, color_type: ColorType::Spot }, + locked: false, + ..cyan.clone() + }; + assert_eq!(row_label("it", &spot.name, row_marks(&spot)), "Pantone, Tinta piatta, RGB"); + } /// The Swatches panel's right-hand columns, as InDesign's: [None], [Registration] and [Black] /// can't be edited, [Paper] can; colours show their type and mode.