Merge pull request #6157 from thisisgomaa/fix/prefs-input-labels

Preferences: inputs expose their labels to accessibility tools
This commit is contained in:
BFlat
2026-11-09 03:19:14 +08:00
committed by GitHub
2 changed files with 113 additions and 17 deletions
+24 -17
View File
@@ -511,10 +511,13 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
}
PrefKind::Bool => bool_row(ui, d, sp.key, sp.label),
PrefKind::Num { min, max, unit } => {
labeled(ui, sp.label, |ui| {
labeled(ui, sp.label, |ui, label_id| {
let mut x = v.as_f64().unwrap_or(min);
let new = if sp.key == "uiScaling" {
widgets::slider(ui, egui::Slider::new(&mut x, min..=max).step_by(0.05).text(tl!("Smaller ↔ Larger"))).changed().then_some(x)
// Without internal text, both slider controls inherit the row's name across focus changes.
let response = widgets::slider(ui, egui::Slider::new(&mut x, min..=max).step_by(0.05)).labelled_by(label_id);
ui.add(egui::Label::new(tl!("Smaller ↔ Larger")).wrap_mode(egui::TextWrapMode::Extend));
response.changed().then_some(x)
} else {
widgets::range_field(ui, sp.key, x, min..=max, &format!(" {unit}"), 3, 110.0)
};
@@ -527,17 +530,19 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
// In the dialog's own Units choice, so a change there shows right away.
let unit = vectorcraft_doc::Unit::named(&d.str(measure.pref_key())).unwrap_or_default();
let x = d.f64(sp.key, min);
labeled(ui, sp.label, |ui| {
labeled(ui, sp.label, |ui, _| {
if let Some(x) = widgets::num_field(ui, sp.key, Some(x), unit, 110.0) {
d.fields.insert(sp.key.into(), json!(x.clamp(min, max)));
}
});
}
PrefKind::Int { min, max } => {
labeled(ui, sp.label, |ui| {
labeled(ui, sp.label, |ui, label_id| {
let mut x = v.as_i64().unwrap_or(min);
let new = if sp.key == "anchorSize" {
widgets::slider(ui, egui::Slider::new(&mut x, min..=max).show_value(false).text(tl!("Size"))).changed().then_some(x)
let response = widgets::slider(ui, egui::Slider::new(&mut x, min..=max).show_value(false)).labelled_by(label_id);
ui.add(egui::Label::new(tl!("Size")).wrap_mode(egui::TextWrapMode::Extend));
response.changed().then_some(x)
} else {
widgets::range_field(ui, sp.key, x as f64, min as f64..=max as f64, "", 0, 110.0).map(|x| x as i64)
};
@@ -569,11 +574,11 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
}
}
PrefKind::Color => {
labeled(ui, sp.label, |ui| {
labeled(ui, sp.label, |ui, label_id| {
let hex = v.as_str().unwrap_or("#000000");
let c = vectorcraft_color::Color::from_hex(hex).map(|c| c.to_rgba8(1.0)).unwrap_or([0, 0, 0, 255]);
let mut rgb = [c[0], c[1], c[2]];
if ui.color_edit_button_srgb(&mut rgb).changed() {
if ui.color_edit_button_srgb(&mut rgb).labelled_by(label_id).changed() {
d.fields.insert(sp.key.into(), json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2])));
}
ui.label(egui::RichText::new(hex).color(t.text_dim).size(11.0));
@@ -581,7 +586,7 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
}
PrefKind::Text if sp.key == "interfaceLanguage" => {
// `auto` plus every registered language, by its own name.
labeled(ui, sp.label, |ui| {
labeled(ui, sp.label, |ui, _| {
let cur = v.as_str().unwrap_or("auto");
let codes: Vec<&str> = std::iter::once("auto").chain(crate::i18n::Lang::for_picker().into_iter().map(|l| l.code())).collect();
let labels: Vec<&str> =
@@ -598,7 +603,7 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
// Plug-ins Folder: typed, or picked with Choose… where the host has a folder picker
// (not on the web).
PrefKind::Text if folder_check(sp.key).is_some() => {
labeled(ui, sp.label, |ui| {
labeled(ui, sp.label, |ui, _| {
let mut s = v.as_str().unwrap_or("").to_string();
if ui.add(egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(200.0)).changed() {
d.fields.insert(sp.key.into(), json!(s));
@@ -609,7 +614,7 @@ fn category_fields(ui: &mut egui::Ui, app: &mut VectorcraftApp, d: &mut Dialog,
});
}
PrefKind::Text => {
labeled(ui, sp.label, |ui| {
labeled(ui, sp.label, |ui, _| {
let mut s = v.as_str().unwrap_or("").to_string();
if ui.add(egui::TextEdit::singleline(&mut s).align(egui::Align2::LEFT_CENTER).desired_width(260.0)).changed() {
d.fields.insert(sp.key.into(), json!(s));
@@ -644,7 +649,7 @@ fn picas_in_use(d: &Dialog) -> bool {
}
fn choice_row(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, opts: &[(&str, &str)], v: &Value) {
labeled(ui, label, |ui| {
labeled(ui, label, |ui, _| {
let cur = v.as_str().unwrap_or("");
let cur_label = opts.iter().find(|o| o.0 == cur).map(|o| o.1).unwrap_or(cur);
let labels: Vec<&str> = opts.iter().map(|o| o.1).collect();
@@ -664,14 +669,16 @@ fn bool_row(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) {
}
}
fn labeled(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui)) {
fn labeled(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui, egui::Id)) {
let t = Tokens::get(ui.ctx());
ui.horizontal(|ui| {
ui.allocate_ui_with_layout(egui::vec2(210.0, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.set_min_width(210.0);
ui.add(egui::Label::new(egui::RichText::new(format!("{}{}", tl!(label), tl!(":"))).color(t.text)).truncate());
});
add(ui);
let label = ui
.allocate_ui_with_layout(egui::vec2(210.0, 22.0), egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.set_min_width(210.0);
ui.add(egui::Label::new(egui::RichText::new(format!("{}{}", tl!(label), tl!(":"))).color(t.text)).truncate())
})
.inner;
ui.label_inputs_by(label.id, |ui| add(ui, label.id));
});
}
+89
View File
@@ -955,6 +955,95 @@ fn ok_refuses_a_typed_folder_that_choose_would_refuse() {
let _ = std::fs::remove_dir_all(&dir);
}
/// Preferences form rows associate each input with its visible label for accessibility tools.
#[test]
fn preference_inputs_expose_their_visible_labels() {
use egui_kittest::kittest::Queryable;
let mut app = app();
open(&mut app, Some("General"));
let mut fonts = false;
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(900.0, 700.0)).with_accessibility_check(false).build_ui_state(
move |ui, app: &mut VectorcraftApp| {
if !std::mem::replace(&mut fonts, true) {
crate::theme::install_fonts(ui.ctx());
return;
}
show(app, ui.ctx());
},
app,
);
h.run_steps(3);
let _ = h.get_by_role_and_label(egui::accesskit::Role::TextInput, "Keyboard Increment:");
}
/// Render the real category without the dialog's scroll viewport clipping later rows.
fn preference_category_harness(category: &'static str) -> egui_kittest::Harness<'static, (VectorcraftApp, Dialog)> {
let mut app = app();
app.services.pick_folder = Some(Box::new(|| None));
open(&mut app, Some(category));
let dialog = app.ui.dialog.take().unwrap();
let mut fonts = false;
let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(900.0, 1400.0)).with_accessibility_check(false).build_ui_state(
move |ui, (app, dialog): &mut (VectorcraftApp, Dialog)| {
if !std::mem::replace(&mut fonts, true) {
crate::theme::install_fonts(ui.ctx());
return;
}
category_fields(ui, app, dialog, category);
},
(app, dialog),
);
h.run_steps(3);
h
}
#[test]
fn preference_colour_well_exposes_its_row_label_and_value() {
use egui_kittest::kittest::{NodeT, Queryable};
let h = preference_category_harness("Guides & Grid");
let expected = format!("{}ff", h.state().1.str("patternTileEdgeColor").to_lowercase());
let colour = h.get_by_role_and_label(egui::accesskit::Role::ColorWell, "Tile Edge Color:");
assert_eq!(colour.accesskit_node().value().as_deref(), Some(expected.as_str()));
}
#[test]
fn preference_scale_labels_both_controls_across_focus_changes() {
use egui::accesskit::Role;
use egui_kittest::kittest::{NodeT, Queryable};
let mut h = preference_category_harness("User Interface");
let expected = h.state().1.f64("uiScaling", 1.0);
for focus in [None, Some(Role::SpinButton), Some(Role::Slider)] {
if let Some(role) = focus {
h.get_by_role_and_label(role, "Scale:").focus();
h.run_steps(3);
assert!(h.get_by_role_and_label(role, "Scale:").accesskit_node().is_focused());
}
for role in [Role::Slider, Role::SpinButton] {
let control = h.get_by_role_and_label(role, "Scale:");
assert_eq!(control.accesskit_node().numeric_value(), Some(expected));
}
}
}
#[test]
fn preference_anchor_slider_exposes_its_row_label_and_value() {
use egui::accesskit::Role;
use egui_kittest::kittest::{NodeT, Queryable};
let h = preference_category_harness("Selection & Anchor Display");
let slider = h.get_by_role_and_label(Role::Slider, "Size:");
assert_eq!(slider.accesskit_node().numeric_value(), Some(h.state().1.f64("anchorSize", 3.0)));
}
#[test]
fn preference_folder_label_preserves_the_choose_action_name() {
use egui::accesskit::Role;
use egui_kittest::kittest::Queryable;
let h = preference_category_harness("File Handling");
let _ = h.get_by_role_and_label(Role::TextInput, "Folder:");
let _ = h.get_by_role_and_label(Role::Button, "Choose…");
assert!(h.query_by_role_and_label(Role::Button, "Folder:").is_none());
}
/// Under automation roots, OK refuses a typed folder outside them before it looks at the folder.
#[test]
fn ok_refuses_a_typed_folder_outside_the_automation_roots_before_looking_at_it() {