mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 13:38:14 -06:00
LRC-LIB-KEYWORDS: chips speak, and keep their menu with their keyword
Drawn as one widget, a chip and its × lost the names a button gave them: screen readers met unlabeled buttons. The chip is now named after its whole keyword (even when cut short on screen) and the × "Remove “…”". Both answer under ids made from their keyword rather than their place in the row, so a chip's open menu stays with its keyword when the chips change underneath it (an undo, a sync). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9dabd38941
commit
f5b1c09e42
@@ -214,5 +214,6 @@
|
||||
"“{parent}” has a “{name}” already. Merge the two?": "„{parent}“ enthält bereits „{name}“. Beide zusammenführen?",
|
||||
"There is a “{name}” at the top level already. Merge the two?": "Auf der obersten Ebene gibt es bereits „{name}“. Beide zusammenführen?",
|
||||
"Added “{keyword}” to {n} photo{}": "„{keyword}“ zu {n} Foto{} hinzugefügt",
|
||||
"On {have} of {of} selected photos": "Auf {have} von {of} ausgewählten Fotos"
|
||||
"On {have} of {of} selected photos": "Auf {have} von {of} ausgewählten Fotos",
|
||||
"Remove “{name}”": "„{name}“ entfernen"
|
||||
}
|
||||
|
||||
@@ -214,5 +214,6 @@
|
||||
"“{parent}” has a “{name}” already. Merge the two?": "“{parent}” ya tiene “{name}”. ¿Combinar las dos?",
|
||||
"There is a “{name}” at the top level already. Merge the two?": "Ya hay “{name}” en el nivel superior. ¿Combinar las dos?",
|
||||
"Added “{keyword}” to {n} photo{}": "Se añadió “{keyword}” a {n} foto{}",
|
||||
"On {have} of {of} selected photos": "En {have} de {of} fotos seleccionadas"
|
||||
"On {have} of {of} selected photos": "En {have} de {of} fotos seleccionadas",
|
||||
"Remove “{name}”": "Quitar “{name}”"
|
||||
}
|
||||
|
||||
@@ -214,5 +214,6 @@
|
||||
"“{parent}” has a “{name}” already. Merge the two?": "「{parent}」にはすでに「{name}」があります。統合しますか?",
|
||||
"There is a “{name}” at the top level already. Merge the two?": "最上位にはすでに「{name}」があります。統合しますか?",
|
||||
"Added “{keyword}” to {n} photo{}": "写真{n}枚に「{keyword}」を追加しました{:.0}",
|
||||
"On {have} of {of} selected photos": "選択した写真{of}枚のうち{have}枚"
|
||||
"On {have} of {of} selected photos": "選択した写真{of}枚のうち{have}枚",
|
||||
"Remove “{name}”": "「{name}」を削除"
|
||||
}
|
||||
|
||||
@@ -214,5 +214,6 @@
|
||||
"“{parent}” has a “{name}” already. Merge the two?": "“{parent}” já tem “{name}”. Mesclar as duas?",
|
||||
"There is a “{name}” at the top level already. Merge the two?": "Já existe “{name}” no nível superior. Mesclar as duas?",
|
||||
"Added “{keyword}” to {n} photo{}": "“{keyword}” adicionada a {n} foto{}",
|
||||
"On {have} of {of} selected photos": "Em {have} de {of} fotos selecionadas"
|
||||
"On {have} of {of} selected photos": "Em {have} de {of} fotos selecionadas",
|
||||
"Remove “{name}”": "Remover “{name}”"
|
||||
}
|
||||
|
||||
@@ -214,5 +214,6 @@
|
||||
"“{parent}” has a “{name}” already. Merge the two?": "В «{parent}» уже есть «{name}». Объединить их?",
|
||||
"There is a “{name}” at the top level already. Merge the two?": "На верхнем уровне уже есть «{name}». Объединить их?",
|
||||
"Added “{keyword}” to {n} photo{}": "«{keyword}» добавлено к фото ({n}){:.0}",
|
||||
"On {have} of {of} selected photos": "На {have} из {of} выбранных фото"
|
||||
"On {have} of {of} selected photos": "На {have} из {of} выбранных фото",
|
||||
"Remove “{name}”": "Убрать «{name}»"
|
||||
}
|
||||
|
||||
@@ -214,5 +214,6 @@
|
||||
"“{parent}” has a “{name}” already. Merge the two?": "“{parent}”中已有“{name}”。要合并两者吗?",
|
||||
"There is a “{name}” at the top level already. Merge the two?": "顶层已有“{name}”。要合并两者吗?",
|
||||
"Added “{keyword}” to {n} photo{}": "已将“{keyword}”添加到 {n} 张照片{:.0}",
|
||||
"On {have} of {of} selected photos": "所选 {of} 张照片中的 {have} 张"
|
||||
"On {have} of {of} selected photos": "所选 {of} 张照片中的 {have} 张",
|
||||
"Remove “{name}”": "移除“{name}”"
|
||||
}
|
||||
|
||||
@@ -214,5 +214,6 @@
|
||||
"“{parent}” has a “{name}” already. Merge the two?": "「{parent}」中已有「{name}」。要合併兩者嗎?",
|
||||
"There is a “{name}” at the top level already. Merge the two?": "最上層已有「{name}」。要合併兩者嗎?",
|
||||
"Added “{keyword}” to {n} photo{}": "已將「{keyword}」加入 {n} 張照片{:.0}",
|
||||
"On {have} of {of} selected photos": "選取的 {of} 張照片中的 {have} 張"
|
||||
"On {have} of {of} selected photos": "選取的 {of} 張照片中的 {have} 張",
|
||||
"Remove “{name}”": "移除「{name}」"
|
||||
}
|
||||
|
||||
@@ -61,22 +61,30 @@ fn chip(ui: &mut egui::Ui, path: &str, partial: bool, cross: bool) -> (egui::Res
|
||||
let measure = |s: &str| ui.painter().layout_no_wrap(s.to_string(), font.clone(), color).size().x;
|
||||
let shown = chip_label(path, partial, room, measure);
|
||||
let w = measure(&shown) + 16.0 + cross_w;
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(w, 22.0), Sense::click());
|
||||
// the row places it; its name and × answer under ids of their own keyword, so an open menu
|
||||
// stays with its keyword when the chips change
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(w, 22.0), Sense::hover());
|
||||
let id = egui::Id::new(("keyword-chip", path.to_lowercase(), cross));
|
||||
let body = Rect::from_min_max(rect.min, pos2(rect.right() - cross_w, rect.bottom()));
|
||||
let full = path.replace('|', " › ");
|
||||
let resp = ui.interact(body, id, Sense::click());
|
||||
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &full));
|
||||
let x = cross.then(|| {
|
||||
let xr = Rect::from_center_size(pos2(rect.right() - 11.0, rect.center().y), vec2(14.0, 14.0));
|
||||
let xresp = ui.interact(xr, id.with("remove"), Sense::click());
|
||||
let said = crate::i18n::tr_format!("Remove “{name}”", name = full);
|
||||
xresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &said));
|
||||
xresp
|
||||
});
|
||||
ui.painter().rect_stroke(rect, 10.0, egui::Stroke::new(1.0, t.field_border), egui::StrokeKind::Inside);
|
||||
if resp.hovered() {
|
||||
if resp.hovered() || x.as_ref().is_some_and(egui::Response::hovered) {
|
||||
ui.painter().rect_filled(rect.shrink(1.0), 10.0, t.hover.gamma_multiply(0.5));
|
||||
}
|
||||
ui.painter().text(pos2(rect.left() + 8.0, rect.center().y), Align2::LEFT_CENTER, &shown, font.clone(), color);
|
||||
let body = Rect::from_min_max(rect.min, pos2(rect.right() - cross_w, rect.bottom()));
|
||||
let x = cross.then(|| {
|
||||
let xr = Rect::from_center_size(pos2(rect.right() - 11.0, rect.center().y), vec2(14.0, 14.0));
|
||||
let xresp = ui.interact(xr, resp.id.with("remove"), Sense::click());
|
||||
ui.painter().text(xr.center(), Align2::CENTER_CENTER, "×", t.font(13.0), if xresp.hovered() { t.text } else { t.text_dim });
|
||||
xresp
|
||||
});
|
||||
let mut body_resp = resp;
|
||||
body_resp.rect = body;
|
||||
(body_resp, x)
|
||||
if let Some(x) = &x {
|
||||
ui.painter().text(x.rect.center(), Align2::CENTER_CENTER, "×", t.font(13.0), if x.hovered() { t.text } else { t.text_dim });
|
||||
}
|
||||
(resp, x)
|
||||
}
|
||||
|
||||
/// A chip's menu: it acts on the selected photos (deleting a keyword from the whole library is the
|
||||
@@ -311,4 +319,27 @@ mod tests {
|
||||
c.apply(Op::SetKeyword { path: "Beach".into(), info: Some(KeywordInfo::default()) }).unwrap();
|
||||
assert_eq!(shown(&chips(&c, &ids)), [("Beach", 1, 1)]);
|
||||
}
|
||||
|
||||
/// Screen readers hear a chip by its whole keyword (even when cut short on screen) and its ×
|
||||
/// as removing that keyword.
|
||||
#[test]
|
||||
fn chips_describe_themselves() {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
ctx.enable_accesskit();
|
||||
let mut found = Vec::new();
|
||||
for _ in 0..3 {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| {
|
||||
ui.set_max_width(120.0);
|
||||
let _ = chip(ui, "Places|Portugal|Lisbon|Belém tower", true, true);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
if let Some(update) = out.platform_output.accesskit_update.take() {
|
||||
found = update.nodes.iter().map(|(_, n)| (format!("{:?}", n.role()), n.label().unwrap_or_default().to_string())).collect();
|
||||
}
|
||||
}
|
||||
let has = |role: &str, label: &str| found.iter().any(|(r, l)| r == role && l == label);
|
||||
assert!(has("Button", "Places › Portugal › Lisbon › Belém tower"), "{found:?}");
|
||||
assert!(has("Button", "Remove “Places › Portugal › Lisbon › Belém tower”"), "{found:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user