diff --git a/crates/ui-egui/locales/cs.json b/crates/ui-egui/locales/cs.json index d3ca026fc..17006b500 100644 --- a/crates/ui-egui/locales/cs.json +++ b/crates/ui-egui/locales/cs.json @@ -2412,5 +2412,8 @@ "Import dropped files without review": "Importovat přetažené soubory bez kontroly", "An import is running": "Import už běží", "An import is running. Drop photos when it has finished": "Import už běží. Fotky přetáhněte, až skončí", - "Finish Lightroom catalog import before adding photos": "Dokončete import katalogu Lightroom, než přidáte fotky" + "Finish Lightroom catalog import before adding photos": "Dokončete import katalogu Lightroom, než přidáte fotky", + "Top bar": "Horní lišta", + "Tool strip": "Pruh nástrojů", + "Bottom bar": "Dolní lišta" } diff --git a/crates/ui-egui/locales/de.json b/crates/ui-egui/locales/de.json index fee77ac5f..2138c1eae 100644 --- a/crates/ui-egui/locales/de.json +++ b/crates/ui-egui/locales/de.json @@ -3444,5 +3444,8 @@ "Import dropped files without review": "Abgelegte Dateien ohne Prüfung importieren", "An import is running": "Ein Import läuft", "An import is running. Drop photos when it has finished": "Ein Import läuft. Lege die Fotos ab, wenn er fertig ist", - "Finish Lightroom catalog import before adding photos": "Schließe den Import des Lightroom-Katalogs ab, bevor du Fotos hinzufügst" + "Finish Lightroom catalog import before adding photos": "Schließe den Import des Lightroom-Katalogs ab, bevor du Fotos hinzufügst", + "Top bar": "Obere Leiste", + "Tool strip": "Werkzeugleiste", + "Bottom bar": "Untere Leiste" } diff --git a/crates/ui-egui/locales/es.json b/crates/ui-egui/locales/es.json index 460fa3a96..a244e6c13 100644 --- a/crates/ui-egui/locales/es.json +++ b/crates/ui-egui/locales/es.json @@ -2362,5 +2362,8 @@ "Import dropped files without review": "Importar los archivos soltados sin revisión", "An import is running": "Hay una importación en curso", "An import is running. Drop photos when it has finished": "Hay una importación en curso. Suelta las fotos cuando termine", - "Finish Lightroom catalog import before adding photos": "Termina la importación del catálogo de Lightroom antes de añadir fotos" + "Finish Lightroom catalog import before adding photos": "Termina la importación del catálogo de Lightroom antes de añadir fotos", + "Top bar": "Barra superior", + "Tool strip": "Barra de herramientas", + "Bottom bar": "Barra inferior" } diff --git a/crates/ui-egui/locales/fi.json b/crates/ui-egui/locales/fi.json index dcfa3794b..4538dbb6f 100644 --- a/crates/ui-egui/locales/fi.json +++ b/crates/ui-egui/locales/fi.json @@ -2331,5 +2331,8 @@ "Drop to open the face model": "Pudota avataksesi kasvomallin", "Dropped Files": "Pudotetut tiedostot", "Finish Lightroom catalog import before adding photos": "Viimeistele Lightroom-luettelon tuonti ennen kuvien lisäämistä", - "Import dropped files without review": "Tuo pudotetut tiedostot ilman tarkistusta" + "Import dropped files without review": "Tuo pudotetut tiedostot ilman tarkistusta", + "Top bar": "Yläpalkki", + "Tool strip": "Työkalupalkki", + "Bottom bar": "Alapalkki" } diff --git a/crates/ui-egui/locales/fr.json b/crates/ui-egui/locales/fr.json index a24ee0266..62da3ce84 100644 --- a/crates/ui-egui/locales/fr.json +++ b/crates/ui-egui/locales/fr.json @@ -2394,5 +2394,8 @@ "Checking recent libraries": "Vérification des bibliothèques récentes", "The photos are imported once a library is open": "Les photos seront importées dès qu'une bibliothèque sera ouverte", "Open a library first, then drop the presets again": "Ouvrez d'abord une bibliothèque, puis déposez à nouveau les préréglages", - "This library couldn't be checked:": "Impossible de vérifier cette bibliothèque :" + "This library couldn't be checked:": "Impossible de vérifier cette bibliothèque :", + "Top bar": "Barre supérieure", + "Tool strip": "Barre d’outils", + "Bottom bar": "Barre inférieure" } diff --git a/crates/ui-egui/locales/id.json b/crates/ui-egui/locales/id.json index 786c0eb4e..fe4caa98e 100644 --- a/crates/ui-egui/locales/id.json +++ b/crates/ui-egui/locales/id.json @@ -2343,5 +2343,8 @@ "Drop to open the face model": "Jatuhkan untuk membuka model wajah", "Dropped Files": "File yang dijatuhkan", "Finish Lightroom catalog import before adding photos": "Selesaikan impor katalog Lightroom sebelum menambahkan foto", - "Import dropped files without review": "Impor file yang dijatuhkan tanpa tinjauan" + "Import dropped files without review": "Impor file yang dijatuhkan tanpa tinjauan", + "Top bar": "Bilah atas", + "Tool strip": "Bilah alat", + "Bottom bar": "Bilah bawah" } diff --git a/crates/ui-egui/locales/it.json b/crates/ui-egui/locales/it.json index d084d9dea..a064ae8a8 100644 --- a/crates/ui-egui/locales/it.json +++ b/crates/ui-egui/locales/it.json @@ -2365,5 +2365,8 @@ "Import dropped files without review": "Importa i file rilasciati senza revisione", "An import is running": "Un'importazione è in corso", "An import is running. Drop photos when it has finished": "Un'importazione è in corso. Trascina le foto quando è finita", - "Finish Lightroom catalog import before adding photos": "Termina l'importazione del catalogo Lightroom prima di aggiungere foto" + "Finish Lightroom catalog import before adding photos": "Termina l'importazione del catalogo Lightroom prima di aggiungere foto", + "Top bar": "Barra superiore", + "Tool strip": "Barra degli strumenti", + "Bottom bar": "Barra inferiore" } diff --git a/crates/ui-egui/locales/ja.json b/crates/ui-egui/locales/ja.json index 16c9ab89e..044a440b6 100644 --- a/crates/ui-egui/locales/ja.json +++ b/crates/ui-egui/locales/ja.json @@ -2360,5 +2360,8 @@ "Import dropped files without review": "ドロップしたファイルを確認せずに読み込む", "An import is running": "読み込み中です", "An import is running. Drop photos when it has finished": "読み込み中です。完了してから写真をドロップしてください", - "Finish Lightroom catalog import before adding photos": "写真を追加する前に Lightroom カタログの読み込みを完了してください" + "Finish Lightroom catalog import before adding photos": "写真を追加する前に Lightroom カタログの読み込みを完了してください", + "Top bar": "上部バー", + "Tool strip": "ツールバー", + "Bottom bar": "下部バー" } diff --git a/crates/ui-egui/locales/ka.json b/crates/ui-egui/locales/ka.json index 5e9743b9b..cf318ed7c 100644 --- a/crates/ui-egui/locales/ka.json +++ b/crates/ui-egui/locales/ka.json @@ -2738,5 +2738,8 @@ "Import dropped files without review": "ჩაგდებული ფაილების იმპორტი გადახედვის გარეშე", "An import is running": "იმპორტი მიმდინარეობს", "An import is running. Drop photos when it has finished": "იმპორტი მიმდინარეობს. ფოტოები ჩააგდეთ, როცა დასრულდება", - "Finish Lightroom catalog import before adding photos": "ფოტოების დამატებამდე დაასრულეთ Lightroom-ის კატალოგის იმპორტი" + "Finish Lightroom catalog import before adding photos": "ფოტოების დამატებამდე დაასრულეთ Lightroom-ის კატალოგის იმპორტი", + "Top bar": "ზედა ზოლი", + "Tool strip": "ხელსაწყოების ზოლი", + "Bottom bar": "ქვედა ზოლი" } diff --git a/crates/ui-egui/locales/pl.json b/crates/ui-egui/locales/pl.json index 8c3d45962..312adff36 100644 --- a/crates/ui-egui/locales/pl.json +++ b/crates/ui-egui/locales/pl.json @@ -2287,5 +2287,8 @@ "Import dropped files without review": "Importuj upuszczone pliki bez przeglądu", "An import is running": "Import jest w toku", "An import is running. Drop photos when it has finished": "Import jest w toku. Upuść zdjęcia, gdy się zakończy", - "Finish Lightroom catalog import before adding photos": "Zakończ import katalogu Lightrooma, zanim dodasz zdjęcia" + "Finish Lightroom catalog import before adding photos": "Zakończ import katalogu Lightrooma, zanim dodasz zdjęcia", + "Top bar": "Górny pasek", + "Tool strip": "Pasek narzędzi", + "Bottom bar": "Dolny pasek" } diff --git a/crates/ui-egui/locales/pt-br.json b/crates/ui-egui/locales/pt-br.json index 92f2f6825..4bbadd171 100644 --- a/crates/ui-egui/locales/pt-br.json +++ b/crates/ui-egui/locales/pt-br.json @@ -2206,5 +2206,8 @@ "Import dropped files without review": "Importar arquivos soltos sem revisão", "An import is running": "Uma importação está em andamento", "An import is running. Drop photos when it has finished": "Uma importação está em andamento. Solte as fotos quando ela terminar", - "Finish Lightroom catalog import before adding photos": "Conclua a importação do catálogo do Lightroom antes de adicionar fotos" + "Finish Lightroom catalog import before adding photos": "Conclua a importação do catálogo do Lightroom antes de adicionar fotos", + "Top bar": "Barra superior", + "Tool strip": "Barra de ferramentas", + "Bottom bar": "Barra inferior" } diff --git a/crates/ui-egui/locales/ru.json b/crates/ui-egui/locales/ru.json index c2d047e24..91748e246 100644 --- a/crates/ui-egui/locales/ru.json +++ b/crates/ui-egui/locales/ru.json @@ -2570,5 +2570,8 @@ "An import is running": "Импорт уже выполняется", "An import is running. Drop photos when it has finished": "Импорт уже выполняется. Перетащите фото, когда он закончится", "Finish Lightroom catalog import before adding photos": "Завершите импорт каталога Lightroom, прежде чем добавлять фото", - "★ Favorites": "★ Избранное" + "★ Favorites": "★ Избранное", + "Top bar": "Верхняя панель", + "Tool strip": "Панель инструментов", + "Bottom bar": "Нижняя панель" } diff --git a/crates/ui-egui/locales/sv.json b/crates/ui-egui/locales/sv.json index dec026fb9..e85d555b7 100644 --- a/crates/ui-egui/locales/sv.json +++ b/crates/ui-egui/locales/sv.json @@ -2433,5 +2433,8 @@ "Cull": "Gallra", "Add Locations": "Lägg till platser", "Create Stacks": "Skapa staplar", - "The Tags button lists placeholders for the template, such as {date} or {seq}. Each file is renamed on disk along with its XMP sidecar; a name that is taken gets -1, -2…": "Knappen Taggar visar platshållare för mallen, till exempel {date} eller {seq}. Varje fil byter namn på disken tillsammans med sin XMP-sidofil; ett namn som redan finns får -1, -2…" + "The Tags button lists placeholders for the template, such as {date} or {seq}. Each file is renamed on disk along with its XMP sidecar; a name that is taken gets -1, -2…": "Knappen Taggar visar platshållare för mallen, till exempel {date} eller {seq}. Varje fil byter namn på disken tillsammans med sin XMP-sidofil; ett namn som redan finns får -1, -2…", + "Top bar": "Övre fältet", + "Tool strip": "Verktygsfält", + "Bottom bar": "Nedre fältet" } diff --git a/crates/ui-egui/locales/uk.json b/crates/ui-egui/locales/uk.json index d2aee5943..c250754c1 100644 --- a/crates/ui-egui/locales/uk.json +++ b/crates/ui-egui/locales/uk.json @@ -2358,5 +2358,8 @@ "Import dropped files without review": "Імпортувати перетягнуті файли без перегляду", "An import is running": "Імпорт уже виконується", "An import is running. Drop photos when it has finished": "Імпорт уже виконується. Перетягніть фото, коли він завершиться", - "Finish Lightroom catalog import before adding photos": "Завершіть імпорт каталогу Lightroom, перш ніж додавати фото" + "Finish Lightroom catalog import before adding photos": "Завершіть імпорт каталогу Lightroom, перш ніж додавати фото", + "Top bar": "Верхня панель", + "Tool strip": "Панель інструментів", + "Bottom bar": "Нижня панель" } diff --git a/crates/ui-egui/locales/vi.json b/crates/ui-egui/locales/vi.json index c118248aa..ff0ca84f5 100644 --- a/crates/ui-egui/locales/vi.json +++ b/crates/ui-egui/locales/vi.json @@ -2239,5 +2239,8 @@ "Drop to open the face model": "Thả để mở mô hình khuôn mặt", "Dropped Files": "Tệp đã thả", "Finish Lightroom catalog import before adding photos": "Hoàn tất nhập danh mục Lightroom trước khi thêm ảnh", - "Import dropped files without review": "Nhập các tệp đã thả mà không xem lại" + "Import dropped files without review": "Nhập các tệp đã thả mà không xem lại", + "Top bar": "Thanh trên cùng", + "Tool strip": "Thanh công cụ", + "Bottom bar": "Thanh dưới cùng" } diff --git a/crates/ui-egui/locales/zh-hans.json b/crates/ui-egui/locales/zh-hans.json index c09bfe610..74510096d 100644 --- a/crates/ui-egui/locales/zh-hans.json +++ b/crates/ui-egui/locales/zh-hans.json @@ -2204,5 +2204,8 @@ "Import dropped files without review": "不经检查直接导入拖入的文件", "An import is running": "正在导入", "An import is running. Drop photos when it has finished": "正在导入。导入完成后再拖放照片", - "Finish Lightroom catalog import before adding photos": "添加照片前,请先完成 Lightroom 目录的导入" + "Finish Lightroom catalog import before adding photos": "添加照片前,请先完成 Lightroom 目录的导入", + "Top bar": "顶部栏", + "Tool strip": "工具栏", + "Bottom bar": "底部栏" } diff --git a/crates/ui-egui/locales/zh-hant.json b/crates/ui-egui/locales/zh-hant.json index d567bf87e..12aad27a0 100644 --- a/crates/ui-egui/locales/zh-hant.json +++ b/crates/ui-egui/locales/zh-hant.json @@ -2161,5 +2161,8 @@ "Import dropped files without review": "不經檢查直接匯入拖放的檔案", "An import is running": "正在匯入", "An import is running. Drop photos when it has finished": "正在匯入。匯入完成後再拖放照片", - "Finish Lightroom catalog import before adding photos": "新增照片前,請先完成 Lightroom 目錄的匯入" + "Finish Lightroom catalog import before adding photos": "新增照片前,請先完成 Lightroom 目錄的匯入", + "Top bar": "頂部列", + "Tool strip": "工具列", + "Bottom bar": "底部列" } diff --git a/crates/ui-egui/src/docking.rs b/crates/ui-egui/src/docking.rs index f2bc190d7..bbc05c497 100644 --- a/crates/ui-egui/src/docking.rs +++ b/crates/ui-egui/src/docking.rs @@ -402,17 +402,24 @@ pub fn show(app: &mut crate::LightcraftApp, ui: &mut egui::Ui) { panel_limits, |ui, panel| match panel { Panel::Canvas => { - canvas = Some(ui.new_child(egui::UiBuilder::new().id_salt("photo-view").max_rect(ui.max_rect()))); + let child = ui.new_child(egui::UiBuilder::new().id_salt("photo-view").max_rect(ui.max_rect())); + crate::widgets::announce_region(&child, egui::accesskit::Role::Main, crate::i18n::tr(Panel::Canvas.label())); + canvas = Some(child); } Panel::Library => { + crate::widgets::announce_region(ui, egui::accesskit::Role::Navigation, crate::i18n::tr(Panel::Library.label())); crate::widgets::register(ui, "panel:left_panel", ui.max_rect()); crate::panels::left::show(app, ui); } Panel::Tools => { + crate::widgets::announce_region(ui, egui::accesskit::Role::Complementary, crate::i18n::tr(Panel::Tools.label())); crate::widgets::register(ui, "panel:right_panel", ui.max_rect()); crate::panels::right::show(app, ui); } - Panel::Presets => crate::panels::presets::show(app, ui), + Panel::Presets => { + crate::widgets::announce_region(ui, egui::accesskit::Role::Complementary, crate::i18n::tr(Panel::Presets.label())); + crate::panels::presets::show(app, ui); + } }, ); if let Some(mut canvas) = canvas { diff --git a/crates/ui-egui/src/panels/bottombar.rs b/crates/ui-egui/src/panels/bottombar.rs index 0b48d1e34..e52f202d5 100644 --- a/crates/ui-egui/src/panels/bottombar.rs +++ b/crates/ui-egui/src/panels/bottombar.rs @@ -25,6 +25,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { .exact_size(t.bottom_bar_h) .frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin { left: 12, right: 12, top: 0, bottom: 0 })) .show(ui, |ui| { + crate::widgets::announce_region(ui, egui::accesskit::Role::Toolbar, crate::i18n::tr("Bottom bar")); let full = ui.max_rect(); let compact_zoom = full.width() < 360.0; let right_start = right_side(app, ui, full, compact_zoom); diff --git a/crates/ui-egui/src/panels/detail/filmstrip.rs b/crates/ui-egui/src/panels/detail/filmstrip.rs index 4a0de28cf..a03178556 100644 --- a/crates/ui-egui/src/panels/detail/filmstrip.rs +++ b/crates/ui-egui/src/panels/detail/filmstrip.rs @@ -65,10 +65,14 @@ pub(crate) fn filmstrip(app: &mut LightcraftApp, ui: &mut egui::Ui, r: Rect) { let ppp = ui.ctx().pixels_per_point(); if ids.is_empty() { let why = if app.session.filter != Default::default() { "No photos match the filters (View → Clear Filters)" } else { "No photos" }; - ui.painter().text(r.center(), Align2::CENTER_CENTER, why, t.font(12.5), t.text_dim); + // a label, not painted text, so a screen reader reads the reason; the region keeps the strip navigable + let mut empty = ui.new_child(egui::UiBuilder::new().max_rect(r).layout(egui::Layout::centered_and_justified(egui::Direction::TopDown))); + crate::widgets::announce_region(&empty, egui::accesskit::Role::Region, crate::i18n::tr("Filmstrip")); + empty.add(egui::Label::new(egui::RichText::new(why).font(t.font(12.5)).color(t.text_dim)).selectable(false)); return; } let mut child = ui.new_child(egui::UiBuilder::new().max_rect(r.shrink2(vec2(0.0, 4.0)))); + crate::widgets::announce_region(&child, egui::accesskit::Role::Region, crate::i18n::tr("Filmstrip")); // a vertical mouse wheel scrolls the strip sideways (horizontal trackpad scrolls still work) child.style_mut().always_scroll_the_only_direction = true; // only a new active photo (or the strip appearing) scrolls it; see `grid::follow_active` diff --git a/crates/ui-egui/src/panels/strip.rs b/crates/ui-egui/src/panels/strip.rs index 164204b86..faf527be9 100644 --- a/crates/ui-egui/src/panels/strip.rs +++ b/crates/ui-egui/src/panels/strip.rs @@ -26,6 +26,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { .stroke(egui::Stroke::new(1.0, t.divider)), ) .show(ui, |ui| { + crate::widgets::announce_region(ui, egui::accesskit::Role::Toolbar, crate::i18n::tr("Tool strip")); let has_photo = app.session.active().is_some(); ui.vertical_centered(|ui| { ui.spacing_mut().item_spacing.y = GAP; diff --git a/crates/ui-egui/src/panels/topbar.rs b/crates/ui-egui/src/panels/topbar.rs index 986aec38d..4511b4b7c 100644 --- a/crates/ui-egui/src/panels/topbar.rs +++ b/crates/ui-egui/src/panels/topbar.rs @@ -32,6 +32,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { .exact_size(t.top_bar_h) .frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin { left, right: 12, top: 0, bottom: 0 })) .show(ui, |ui| { + crate::widgets::announce_region(ui, egui::accesskit::Role::Toolbar, crate::i18n::tr("Top bar")); let full = ui.max_rect(); // The whole bar, its margins included. The search field is centred on this and not on // `full`, which is only what the margins leave: on macOS the left margin holds the diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 73c574308..18da662eb 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -1208,6 +1208,20 @@ pub(crate) fn screen_reader_name(ctx: &egui::Context, id: egui::Id, name: &str) ctx.accesskit_node_builder(id, |node| node.set_label(name.to_string())); } +/// Names the part of the window `ui` draws for screen readers: a toolbar, or a landmark (VoiceOver's +/// rotor lists the landmarks and jumps to one). Set on the node the `Ui` already has, so nothing is +/// laid out differently. The node gets the `Ui`'s area as its bounds, so a screen reader's cursor +/// lands on the part and not on an empty spot. +pub(crate) fn announce_region(ui: &Ui, role: egui::accesskit::Role, name: impl Into) { + let name = name.into(); + let r = ui.max_rect(); + ui.ctx().accesskit_node_builder(ui.unique_id(), |node| { + node.set_role(role); + node.set_label(name); + node.set_bounds(egui::accesskit::Rect { x0: f64::from(r.min.x), y0: f64::from(r.min.y), x1: f64::from(r.max.x), y1: f64::from(r.max.y) }); + }); +} + /// A hand-drawn clickable area (an icon, a tile, a row) announced as a button named `name`. pub(crate) fn announce(resp: &egui::Response, name: impl Into) { let name = name.into(); diff --git a/crates/ui-egui/tests/it/landmarks.rs b/crates/ui-egui/tests/it/landmarks.rs new file mode 100644 index 000000000..dd98e4c88 --- /dev/null +++ b/crates/ui-egui/tests/it/landmarks.rs @@ -0,0 +1,90 @@ +//! The window's parts are named for screen readers: the three bars are toolbars, and the sidebar, the +//! photos, the tool panel and the filmstrip are landmarks, so VoiceOver's rotor lists them and a +//! person can jump to one instead of walking every control. +use egui::accesskit::Role; +use egui::vec2; +use egui_kittest::{Harness, kittest::NodeT}; +use lightcraft_ui_egui::{ + LightcraftApp, Services, + state::{RightPanel, ViewMode}, +}; + +fn harness(view: ViewMode) -> Harness<'static, LightcraftApp> { + let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services { png: None, ..Default::default() }); + app.ui.left_panel = true; + app.ui.view = view; + app.ui.right = RightPanel::Edit; + let mut h = Harness::builder().with_size(vec2(1500.0, 950.0)).build_ui_state( + |ui, app: &mut LightcraftApp| { + app.logic(&ui.ctx().clone()); + app.ui(ui); + }, + app, + ); + h.run_steps(10); + h +} + +/// The area each node with this role and this name covers (`None`: it reports none). +fn areas(h: &Harness<'_, LightcraftApp>, role: Role, name: &str) -> Vec> { + let nodes = h.root().children_recursive().map(|n| n.accesskit_node()); + nodes.filter(|n| n.role() == role && n.label().as_deref() == Some(name)).map(|n| n.bounding_box()).collect() +} + +/// How many nodes have this role and this name. +fn count(h: &Harness<'_, LightcraftApp>, role: Role, name: &str) -> usize { + areas(h, role, name).len() +} + +#[test] +fn the_bars_are_toolbars_and_the_panels_are_landmarks_in_detail() { + let h = harness(ViewMode::Detail); + for (role, name) in [ + (Role::Toolbar, "Top bar"), + (Role::Toolbar, "Tool strip"), + (Role::Toolbar, "Bottom bar"), + (Role::Navigation, "Library"), + (Role::Main, "Photos"), + (Role::Complementary, "Tools"), + (Role::Region, "Filmstrip"), + ] { + assert_eq!(count(&h, role, name), 1, "one {role:?} named {name:?}"); + let area = areas(&h, role, name).into_iter().next().flatten(); + assert!(area.is_some_and(|b| b.width() > 0.0 && b.height() > 0.0), "{name:?} reports the area it covers: {area:?}"); + } +} + +#[test] +fn the_grid_is_the_main_landmark_in_the_photo_grid() { + let h = harness(ViewMode::PhotoGrid); + assert_eq!(count(&h, Role::Main, "Photos"), 1); + assert_eq!(count(&h, Role::Region, "Filmstrip"), 0, "the grid has no filmstrip"); +} + +/// A filter that hides every photo leaves the filmstrip drawn with its reason: it stays a landmark +/// and the reason is text a screen reader reads, not paint. +#[test] +fn an_empty_filmstrip_is_still_a_landmark_and_says_why() { + let mut session = lightcraft_engine::Session::with_demo(); + session.execute("library.filter", &serde_json::json!({"keyword": "no-such-keyword"})).expect("the filter is set"); + assert!(session.visible_cloned().is_empty(), "the filter hides every photo"); + let mut app = LightcraftApp::new(session, Services { png: None, ..Default::default() }); + app.ui.left_panel = true; + app.ui.view = ViewMode::Detail; + app.ui.right = RightPanel::Edit; + let mut h = Harness::builder().with_size(vec2(1500.0, 950.0)).build_ui_state( + |ui, app: &mut LightcraftApp| { + app.logic(&ui.ctx().clone()); + app.ui(ui); + }, + app, + ); + h.run_steps(10); + assert_eq!(count(&h, Role::Region, "Filmstrip"), 1, "the empty strip is still a landmark"); + let said = h + .root() + .children_recursive() + .filter_map(|n| n.accesskit_node().value().or_else(|| n.accesskit_node().label())) + .any(|t| t.starts_with("No photos match the filters")); + assert!(said, "the reason is read, not only painted"); +} diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs index 75ca8d1bc..764bcfeb0 100644 --- a/crates/ui-egui/tests/it/main.rs +++ b/crates/ui-egui/tests/it/main.rs @@ -4,3 +4,4 @@ mod accessibility; mod dialog_role; mod docking_workflows; +mod landmarks; diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 41e0c47c6..fdabcae2c 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-BEHAV-ACCESS: every dialog and prompt is exposed as a dialog (a modal one, an alert dialog for the prompts); LR-BEHAV-ACCESS: icon-only buttons are named by their label, with the shortcut as the description; MENU-HELP-HELP: on macOS the Help menu is registered with the application only once the menu bar is installed (muda ignores the call before), so AppKit knows which menu is Help; LR-BEHAV-ACCESS: a slider's value field is named ` value`, with how to use it as its description; MENU-BAR: every shortcut the app shows on macOS lists its modifiers ⌃⌥⇧⌘, as the menu bar does; MENU-BAR: on macOS two menu rows that run the same command each get their own native row and label (File ▸ Export… and File ▸ Export with Preset ▸ Custom…; File ▸ New Album… and Photo ▸ Add to Album ▸ New Album…); LR-BEHAV-DIALOGS: a one-line prompt that is refused stays open and says why, and a New Folder on Disk says it was made; LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-BEHAV-ACCESS: the bars are toolbars and the panels landmarks for screen readers; LR-BEHAV-ACCESS: every dialog and prompt is exposed as a dialog (a modal one, an alert dialog for the prompts); LR-BEHAV-ACCESS: icon-only buttons are named by their label, with the shortcut as the description; MENU-HELP-HELP: on macOS the Help menu is registered with the application only once the menu bar is installed (muda ignores the call before), so AppKit knows which menu is Help; LR-BEHAV-ACCESS: a slider's value field is named ` value`, with how to use it as its description; MENU-BAR: every shortcut the app shows on macOS lists its modifiers ⌃⌥⇧⌘, as the menu bar does; MENU-BAR: on macOS two menu rows that run the same command each get their own native row and label (File ▸ Export… and File ▸ Export with Preset ▸ Custom…; File ▸ New Album… and Photo ▸ Add to Album ▸ New Album…); LR-BEHAV-DIALOGS: a one-line prompt that is refused stays open and says why, and a New Folder on Disk says it was made; LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -481,6 +481,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-BEHAV-HEADLESS | Headless UI snapshots | P2 | ✅ | `apps/lightcraft-cli/src/main.rs`, `crates/ui-egui/src/headless.rs`, `docs/control-protocol.md` | `snapshot` rasterizes the UI and defaults photo development to the CPU before the first frame, without adapter discovery; scaled dimensions are validated before allocation, and UI zoom preserves the requested physical viewport; JSON-lines control scripts are supported | | LR-BEHAV-ACCESS | Accessibility | P2 | 🟡 | `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/widgets/text_field.rs`, `crates/ui-egui/src/panels/grid.rs`, `crates/ui-egui/tests/it/accessibility.rs` | AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control, value, range and step and take Increment / Decrement (the value field beside each is named ` value` and describes how to type or drag), buttons / icon buttons / dropdowns / section headers / sources their labels and state, text fields their caption, grid thumbnails file name, rating, flag and label. `tests/it/accessibility.rs` fails on an unnamed control, a slider without range or an untitled dialog in every view, Develop tool and 13 dialogs, and checks that Crop and Remove sliders are reached with Tab and that Tab walks the right panels top to bottom. Canvas tools by keyboard (`crates/ui-egui/src/canvas_keys.rs`, `crates/ui-egui/src/tests/canvas_keys.rs`): crop corners, edges and box, spot targets and sources, a New spot point (Return adds), radial / linear gradient handles and mask pins take the focus, are announced with their position and move with the arrows (Shift: further). Dialogs with one name field (New Album, New Smart Album, Save Metadata Preset, the renames) take the focus once when they open, so Tab and Shift+Tab reach their check box, Cancel and OK, and Return and Esc still confirm and cancel (`crates/ui-egui/src/tests/dialog_tab.rs`). Brush strokes and red-eye ellipses are pointer-only; not tried with a real screen reader ([audit](ui-parity.md#accessibility-audit)) | | LR-BEHAV-ACCESS | Accessibility | P2 | 🟡 | `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/widgets/text_field.rs`, `crates/ui-egui/src/panels/grid.rs`, `crates/ui-egui/tests/it/accessibility.rs` | AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control, value, range and step and take Increment / Decrement, buttons / icon buttons / dropdowns / section headers / sources their labels and state, text fields their caption, grid thumbnails file name, rating, flag and label. `tests/it/accessibility.rs` fails on an unnamed control, a slider without range or an untitled dialog in every view, Develop tool and 13 dialogs, and checks that Crop and Remove sliders are reached with Tab and that Tab walks the right panels top to bottom. Canvas tools by keyboard (`crates/ui-egui/src/canvas_keys.rs`, `crates/ui-egui/src/tests/canvas_keys.rs`): crop corners, edges and box, spot targets and sources, a New spot point (Return adds), radial / linear gradient handles and mask pins take the focus, are announced with their position and move with the arrows (Shift: further). Dialogs with one name field (New Album, New Smart Album, Save Metadata Preset, the renames) take the focus once when they open, so Tab and Shift+Tab reach their check box, Cancel and OK, and Return and Esc still confirm and cancel (`crates/ui-egui/src/tests/dialog_tab.rs`). Icon-only buttons (the tool strip, the view and Before/After / Filmstrip buttons of the bottom bar, the top bar's Import, the mask overlay and White Balance Selector buttons, the slideshow controls; `crates/ui-egui/src/panels/topbar.rs`) are named by their bare label, and the tooltip's key hint ("Edit (E)" → name "Edit", description "E") is their description, which VoiceOver reads after a pause instead of on every visit. Brush strokes and red-eye ellipses are pointer-only; not tried with a real screen reader ([audit](ui-parity.md#accessibility-audit)) | +| LR-BEHAV-ACCESS | Accessibility | P2 | 🟡 | `crates/ui-egui/src/widgets.rs`, `crates/ui-egui/src/widgets/text_field.rs`, `crates/ui-egui/src/panels/grid.rs`, `crates/ui-egui/tests/it/accessibility.rs` | AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control, value, range and step and take Increment / Decrement, buttons / icon buttons / dropdowns / section headers / sources their labels and state, text fields their caption, grid thumbnails file name, rating, flag and label. `tests/it/accessibility.rs` fails on an unnamed control, a slider without range or an untitled dialog in every view, Develop tool and 13 dialogs, and checks that Crop and Remove sliders are reached with Tab and that Tab walks the right panels top to bottom. Canvas tools by keyboard (`crates/ui-egui/src/canvas_keys.rs`, `crates/ui-egui/src/tests/canvas_keys.rs`): crop corners, edges and box, spot targets and sources, a New spot point (Return adds), radial / linear gradient handles and mask pins take the focus, are announced with their position and move with the arrows (Shift: further). Dialogs with one name field (New Album, New Smart Album, Save Metadata Preset, the renames) take the focus once when they open, so Tab and Shift+Tab reach their check box, Cancel and OK, and Return and Esc still confirm and cancel (`crates/ui-egui/src/tests/dialog_tab.rs`). The top bar, tool strip and bottom bar are announced as toolbars and the sidebar, photos, Tools / Presets panel and filmstrip as landmarks (`widgets::announce_region`, `crates/ui-egui/tests/it/landmarks.rs`). Brush strokes and red-eye ellipses are pointer-only; not tried with a real screen reader ([audit](ui-parity.md#accessibility-audit)) | | LR-BEHAV-LOCALE | Language options | P2 | ✅ | `cmd:app.language.english`, `cmd:app.language.simplifiedChinese`, `cmd:app.language.traditionalChinese`, `cmd:app.language.japanese`, `cmd:app.language.portuguese`, `cmd:app.language.french`, `cmd:app.language.spanish`, `cmd:app.language.german`, `cmd:app.language.russian`, `cmd:app.language.ukrainian`, `cmd:app.language.georgian`, `cmd:app.language.polish`, `cmd:app.language.czech`, `cmd:app.language.italian`, `cmd:app.language.swedish`, `cmd:app.language.finnish`, `cmd:app.language.indonesian`, `cmd:app.language.vietnamese` | Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md) | | LR-BEHAV-LOCALIZE | Localisation | P2 | 🟡 | `crates/ui-egui/src/i18n.rs`, `crates/ui-egui/locales/`, `docs/localization.md`, `docs/localization-zh-hans.md`, `docs/localization-zh-hant.md`, `docs/localization-ja.md`, `docs/localization-pt-br.md`, `docs/localization-es.md`, `docs/localization-de.md`, `docs/localization-ru.md`, `docs/localization-ka.md`, `docs/localization-pl.md` | One table entry per language (code, endonym, ISO 15924 script, catalog): English, Simplified Chinese, Traditional Chinese (Taiwan), Japanese, Brazilian Portuguese, Spanish, German, Russian, French, Ukrainian, Polish, Czech, Italian, Georgian, Swedish, Finnish, Indonesian and Vietnamese UI with a persisted language preference and craft-fonts regular/bold faces per script (a variable face, as for Arabic, is drawn heavier for bold from its one file); menus, edit, crop/masks, settings, import/export and primary progress messages. Adding a language registers the locale and its language command, plus two catalogs. Stock presets/profiles, history steps, library headings, date headings and capture times are localised (user-named items stay verbatim); the Ukrainian catalog covers every existing catalog key, command/control label and rule-editor label; the German catalog reports newly added gaps; the other catalogs fall back to English for messages they lack. Technical errors remain English; release notes are translated in German and Ukrainian; craft-fonts faces for other scripts (Thai, Arabic, …) are UI fallbacks in every language, for file, folder and album names (#736); Traditional Chinese borrows the Simplified Chinese face (no TC face in craft-fonts yet), and the web build embeds the Japanese, Thai and Georgian faces in WASM while loading the Simplified Chinese face from a separate font file. | | LR-BEHAV-LEARN | Tutorials | OOS | 🚫 | | | @@ -970,3 +971,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | 2026-10-27 | patch | MENU-HELP-HELP: on macOS the native Help menu is registered with the application (`NSApp.helpMenu`) only once the menu bar is installed (muda ignores the call before, so before it never was), so AppKit knows which menu is Help; status unchanged (✅) | | 2026-10-28 | patch | LR-BEHAV-ACCESS: an icon-only button's accessible name is its label without the key hint, and the hint ("E", "Shift+P", the platform's own glyphs for Import) is its accessible description; the tooltip is unchanged; status unchanged (🟡) | | 2026-10-28 | patch | LR-BEHAV-ACCESS: every dialog, the library chooser and the quit, library-problem and settings-file prompts are exposed as a modal dialog (an alert dialog for the prompts), and the update check's answer as a non-modal one, instead of a group with the title, and an alert dialog carries what it says (the library problem's path, error and explanation; a prompt's text) as its description (`tests/it/dialog_role.rs`); not tried with a real screen reader; status stays 🟡 | +| 2026-10-28 | patch | LR-BEHAV-ACCESS: the top bar, tool strip and bottom bar are announced as toolbars and the sidebar, photos, Tools / Presets panel and filmstrip as landmarks, each with a name; status stays 🟡 | diff --git a/docs/whats-new.md b/docs/whats-new.md index 4ae2544a3..57aa625f5 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -28,6 +28,13 @@ ## 2026-10-28 +### The window's parts are named for screen readers +- The top bar, the tool strip and the bottom bar are now announced as toolbars (Top bar, Tool strip, Bottom bar), and the + sidebar (Library), the photos, the Tools panel (or Presets) and the filmstrip as landmarks, so a screen reader can list + them and jump to one instead of walking every control. Nothing on screen changes. Not yet tried with a real VoiceOver session. + +## 2026-10-28 + ### Dialogs are exposed to screen readers as dialogs - Export, Settings and the other dialogs, the library chooser and the prompts that stop the window (the quit question, a library that could not be opened, a settings file problem) are now exposed to screen readers as dialogs: modal ones, so the