mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 22:28:09 +00:00
Merge pull request #6157 from thisisgomaa/fix/prefs-input-labels
Preferences: inputs expose their labels to accessibility tools
This commit is contained in:
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user