diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index c1eb9cb43..01a70ca97 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -322,8 +322,9 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { if ui.link(egui::RichText::new(tl!("Style:")).size(12.0).color(t.text).underline()).clicked() { app.ui.open_panel = Some("graphicStyles".into()); } - let resp = widgets::chip_button(ui, 22.0, true, |ui, r| crate::panels::graphic_styles::paint_linked(app, ui, r)) - .on_hover_text(tl!("Graphic Style")); + let resp = widgets::chip_button_named(ui, 22.0, true, tl!("Graphic Style"), |ui, r| { + crate::panels::graphic_styles::paint_linked(app, ui, r) + }); egui::Popup::menu(&resp).show(|ui| crate::panels::graphic_styles::picker(app, ui)); // Recolor Artwork, when the selection has colours to recolour. let colored = app.session.active().is_some_and(|st| crate::canvas::has_colors(&st.doc, &st.selection.objects)); @@ -681,16 +682,16 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let nab = app.session.active().map(|d| d.doc.artboards.len()).unwrap_or(0); let cur = crate::panels::artboards::selected(app, nab); let mut go = None; - for (icon, to) in [("chevrons-left", "first"), ("chevron-left", "previous")] { - if widgets::icon_button_enabled(ui, icon, "", false, cur > 0, 18.0).clicked() { + for (icon, to, tip) in [("chevrons-left", "first", tl!("First Artboard")), ("chevron-left", "previous", tl!("Previous Artboard"))] { + if widgets::icon_button_enabled(ui, icon, tip, false, cur > 0, 18.0).clicked() { go = Some(json!(to)); } } if let Some(i) = artboard_list(app, ui, cur) { go = Some(json!(i)); } - for (icon, to) in [("chevron-right", "next"), ("chevrons-right", "last")] { - if widgets::icon_button_enabled(ui, icon, "", false, cur + 1 < nab, 18.0).clicked() { + for (icon, to, tip) in [("chevron-right", "next", tl!("Next Artboard")), ("chevrons-right", "last", tl!("Last Artboard"))] { + if widgets::icon_button_enabled(ui, icon, tip, false, cur + 1 < nab, 18.0).clicked() { go = Some(json!(to)); } } @@ -1132,6 +1133,20 @@ mod tests { assert!(app.run("view.goToArtboard", json!({"index": "sideways"})).is_err()); } + /// Screen readers name the artboard navigator's four buttons, the ones that lead nowhere from + /// the first artboard disabled. + #[test] + fn screen_readers_name_the_artboard_navigators_buttons() { + use crate::widgets::accessibility_tests::{named, tree}; + let mut app = crate::VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 100, "artboards": 3})).unwrap(); + let tree = tree(|ui| super::status_bar(&mut app, ui)); + for (label, enabled) in [("First Artboard", false), ("Previous Artboard", false), ("Next Artboard", true), ("Last Artboard", true)] { + let n = named(&tree, label); + assert_eq!((n.role(), n.is_disabled()), (egui::accesskit::Role::Button, !enabled), "{label}"); + } + } + /// One headless frame of the status bar, with room above it for the artboard list; returns the /// texts painted and where they are. fn status_texts(app: &mut crate::VectorcraftApp, ctx: &egui::Context, events: Vec) -> Vec<(String, egui::Rect)> { diff --git a/crates/ui-egui/src/dialogs/brush_options.rs b/crates/ui-egui/src/dialogs/brush_options.rs index bc9964495..354bbb358 100644 --- a/crates/ui-egui/src/dialogs/brush_options.rs +++ b/crates/ui-egui/src/dialogs/brush_options.rs @@ -207,7 +207,7 @@ fn bristle_body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { }); let rail = Tokens::get(ui.ctx()).input_border; for (key, text, (lo, hi)) in BRISTLE { - widgets::label_row(ui, tl!(text), LABEL_W, |ui| form::slider_field(ui, d, key, lo..=hi, "%", &|_| rail)); + widgets::label_row(ui, tl!(text), LABEL_W, |ui| form::slider_field_named(ui, d, key, tl!(text), lo..=hi, "%", &|_| rail)); } ui.add_space(8.0); let p = params(d); diff --git a/crates/ui-egui/src/dialogs/color_picker.rs b/crates/ui-egui/src/dialogs/color_picker.rs index 1d27072d4..ea1131bb8 100644 --- a/crates/ui-egui/src/dialogs/color_picker.rs +++ b/crates/ui-egui/src/dialogs/color_picker.rs @@ -211,7 +211,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { } ui.add_space(6.0); let track = |z: f32| c32(&Picked::of(slider_color(channel, z, x, y)).snapped(web).color); - if let (Some(z), _) = widgets::channel_slider(ui, "cp-slider", z, vec2(22.0, FIELD), &track) { + if let (Some(z), _) = widgets::channel_slider_named(ui, "cp-slider", tl!(channel.label()), z, vec2(22.0, FIELD), &track) { pick = Some(field_pick(channel, z, x, y)); } } diff --git a/crates/ui-egui/src/dialogs/effect.rs b/crates/ui-egui/src/dialogs/effect.rs index c103da554..669532ef2 100644 --- a/crates/ui-egui/src/dialogs/effect.rs +++ b/crates/ui-egui/src/dialogs/effect.rs @@ -428,7 +428,7 @@ fn zig_zag_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Unit widgets::subheader(ui, tl!("Options")); // Size is a share of the object's size (Relative) or a distance (Absolute). widgets::label_row(ui, tl!("Size:"), LABEL_W, |ui| { - form::slider_rail(ui, d, "size", 0.0..=100.0, &track); + form::slider_rail_named(ui, d, "size", tl!("Size:"), 0.0..=100.0, &track); if !relative { form::length(ui, d, "size", unit, FIELD_W); } else if let Some(n) = widgets::range_field(ui, ("zz", "size"), d.f64("size", 10.0), 0.0..=100.0, "%", 0, FIELD_W) { @@ -444,7 +444,7 @@ fn zig_zag_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: vectorcraft_doc::Unit } }); widgets::label_row(ui, tl!("Ridges per segment:"), LABEL_W, |ui| { - form::slider_rail(ui, d, "ridges", 0.0..=100.0, &track); + form::slider_rail_named(ui, d, "ridges", tl!("Ridges per segment:"), 0.0..=100.0, &track); if let Some(n) = widgets::range_field(ui, ("zz", "ridges"), d.f64("ridges", 4.0), 0.0..=100.0, "", 0, FIELD_W) { d.fields.insert("ridges".into(), json!(n)); } diff --git a/crates/ui-egui/src/dialogs/flatten.rs b/crates/ui-egui/src/dialogs/flatten.rs index 9d73431d7..194f2bf17 100644 --- a/crates/ui-egui/src/dialogs/flatten.rs +++ b/crates/ui-egui/src/dialogs/flatten.rs @@ -191,7 +191,11 @@ pub(crate) fn options_editor(ui: &mut egui::Ui, id: &str, o: &mut FlattenOptions ui.spacing_mut().item_spacing.x = 6.0; dim(ui, tl!("Rasters")); let rail = (ui.available_width() - FIELD - 56.0).clamp(80.0, 200.0); - if let (Some(x), _) = widgets::color_slider(ui, (id, "balance-slider"), (o.balance / 100.0) as f32, rail, &|_| t.input_border) { + if let (Some(x), _) = + widgets::color_slider_named(ui, (id, "balance-slider"), tl!("Raster/Vector Balance:"), (o.balance / 100.0) as f32, rail, &|_| { + t.input_border + }) + { o.balance = (x as f64 * 100.0).round(); } dim(ui, tl!("Vectors")); diff --git a/crates/ui-egui/src/dialogs/form.rs b/crates/ui-egui/src/dialogs/form.rs index 460eda4b2..82b3cb8db 100644 --- a/crates/ui-egui/src/dialogs/form.rs +++ b/crates/ui-egui/src/dialogs/form.rs @@ -218,7 +218,7 @@ pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) { let max = vectorcraft_geom::shapes::GRID_SKEW_MAX; row_label(ui, tl!("Skew:")); ui.vertical(|ui| { - ui.horizontal(|ui| slider_field(ui, d, &k, -max..=max, "%", &|_| rail)); + ui.horizontal(|ui| slider_field_named(ui, d, &k, tl!("Skew:"), -max..=max, "%", &|_| rail)); rail_ends(ui, ends); }); ui.end_row(); @@ -315,7 +315,7 @@ pub(super) fn param_fields( }); } Value::Bool(b) => { - if crate::widgets::check(ui, "", b, true) { + if crate::widgets::check_box(ui, &humanized(&k), b, true) { d.fields.insert(k, json!(!b)); changed = true; } @@ -384,7 +384,7 @@ pub(super) fn schema_fields(ui: &mut egui::Ui, d: &mut Dialog, specs: &[(String, } ParamSpec::Bool { .. } => { let b = cur.as_bool().unwrap_or(false); - crate::widgets::check(ui, "", b, true).then(|| json!(!b)) + crate::widgets::check_box(ui, &humanize(k), b, true).then(|| json!(!b)) } ParamSpec::Choice { options, .. } => { let labels: Vec<&str> = options.iter().map(String::as_str).collect(); @@ -476,11 +476,12 @@ pub(super) fn slider_w( let t = Tokens::get(ui.ctx()); ui.horizontal(|ui| { ui.add_sized([label_w, 22.0], egui::Label::new(egui::RichText::new(tl!(label)).color(t.text))); - slider_field(ui, d, key, range, suffix, track); + slider_field_named(ui, d, key, tl!(label), range, suffix, track); }); } /// The rail and value field of [`slider`] without its label (for a row that draws its own). +#[allow(dead_code)] pub(super) fn slider_field( ui: &mut egui::Ui, d: &mut Dialog, @@ -488,10 +489,23 @@ pub(super) fn slider_field( range: std::ops::RangeInclusive, suffix: &str, track: &dyn Fn(f32) -> egui::Color32, +) { + slider_field_named(ui, d, key, "", range, suffix, track); +} + +/// [`slider_field`] whose rail screen readers call `name` (the row's label, already translated). +pub(super) fn slider_field_named( + ui: &mut egui::Ui, + d: &mut Dialog, + key: &str, + name: &str, + range: std::ops::RangeInclusive, + suffix: &str, + track: &dyn Fn(f32) -> egui::Color32, ) { let (min, max) = (*range.start(), *range.end()); let v = d.f64(key, 0.0).clamp(min, max); - slider_rail(ui, d, key, range, track); + slider_rail_named(ui, d, key, name, range, track); if let Some(n) = crate::widgets::plain_field(ui, ("dlg-field", key), v, suffix, 0, 52.0).map(|x| x.round().clamp(min, max)).filter(|n| *n != v) { d.fields.insert(key.into(), json!(n)); } @@ -499,10 +513,23 @@ pub(super) fn slider_field( /// The rail of [`slider_field`] alone: dragging it sets the number `d.fields[key]` to a whole /// number in `range` (a value past the range shows at its end until the rail moves). +#[allow(dead_code)] pub(super) fn slider_rail(ui: &mut egui::Ui, d: &mut Dialog, key: &str, range: std::ops::RangeInclusive, track: &dyn Fn(f32) -> egui::Color32) { + slider_rail_named(ui, d, key, "", range, track); +} + +/// [`slider_rail`] that screen readers call `name` (already translated). +pub(super) fn slider_rail_named( + ui: &mut egui::Ui, + d: &mut Dialog, + key: &str, + name: &str, + range: std::ops::RangeInclusive, + track: &dyn Fn(f32) -> egui::Color32, +) { let (min, max) = (*range.start(), *range.end()); let v = d.f64(key, 0.0).clamp(min, max); - let (dragged, _) = crate::widgets::color_slider(ui, ("dlg-slider", key), ((v - min) / (max - min)) as f32, SLIDER_WIDTH, track); + let (dragged, _) = crate::widgets::color_slider_named(ui, ("dlg-slider", key), name, ((v - min) / (max - min)) as f32, SLIDER_WIDTH, track); if let Some(n) = dragged.map(|x| (min + x as f64 * (max - min)).round()).filter(|n| *n != v) { d.fields.insert(key.into(), json!(n)); } diff --git a/crates/ui-egui/src/dialogs/freehand.rs b/crates/ui-egui/src/dialogs/freehand.rs index 8918cd079..f87cef445 100644 --- a/crates/ui-egui/src/dialogs/freehand.rs +++ b/crates/ui-egui/src/dialogs/freehand.rs @@ -71,7 +71,7 @@ fn fidelity(ui: &mut egui::Ui, d: &mut Dialog) { let mut new = None; widgets::label_row(ui, tl!("Fidelity:"), label_w, |ui| { let at = ((v.clamp(min, max) - min) / (max - min)) as f32; - if let (Some(x), _) = widgets::color_slider(ui, "freehand-fidelity", at, form::SLIDER_WIDTH, &|_| t.input_border) { + if let (Some(x), _) = widgets::color_slider_named(ui, "freehand-fidelity", tl!("Fidelity:"), at, form::SLIDER_WIDTH, &|_| t.input_border) { new = Some(((min + x as f64 * (max - min)) * 10.0).round() / 10.0); } if let Some(x) = widgets::plain_field(ui, "freehand-fidelity-field", v, "pt", 1, 64.0) { diff --git a/crates/ui-egui/src/dialogs/path_ops.rs b/crates/ui-egui/src/dialogs/path_ops.rs index 8bf15f7a3..162435462 100644 --- a/crates/ui-egui/src/dialogs/path_ops.rs +++ b/crates/ui-egui/src/dialogs/path_ops.rs @@ -111,7 +111,7 @@ fn simplify_fields(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) { }); ui.add_space(8.0); form::row_label(ui, tl!("Corner Point Angle Threshold:")); - ui.horizontal(|ui| form::slider_field(ui, d, "angleThreshold", 0.0..=180.0, "°", &|_| rail)); + ui.horizontal(|ui| form::slider_field_named(ui, d, "angleThreshold", tl!("Corner Point Angle Threshold:"), 0.0..=180.0, "°", &|_| rail)); ui.add_space(8.0); form::check(ui, d, "straightLines", tl!("Convert to Straight Lines")); form::check(ui, d, "showOriginal", tl!("Show Original Path")); diff --git a/crates/ui-egui/src/dialogs/perspective_grid.rs b/crates/ui-egui/src/dialogs/perspective_grid.rs index b6d6af22d..5734ca7fc 100644 --- a/crates/ui-egui/src/dialogs/perspective_grid.rs +++ b/crates/ui-egui/src/dialogs/perspective_grid.rs @@ -332,8 +332,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { widgets::label_row(ui, tl!("Opacity:"), LABEL, |ui| { let v = d.f64("opacity", 50.0).clamp(0.0, 100.0); let track = |x: f32| egui::Color32::from_gray((60.0 + x * 160.0) as u8); - let mut new = - widgets::color_slider(ui, "persp-opacity", (v / 100.0) as f32, form::SLIDER_WIDTH, &track).0.map(|x| (f64::from(x) * 100.0).round()); + let mut new = widgets::color_slider_named(ui, "persp-opacity", tl!("Opacity:"), (v / 100.0) as f32, form::SLIDER_WIDTH, &track) + .0 + .map(|x| (f64::from(x) * 100.0).round()); if let Some(x) = widgets::plain_field(ui, "persp-opacity-field", v, "%", 0, 52.0) { new = Some(x.round().clamp(0.0, 100.0)); } diff --git a/crates/ui-egui/src/dialogs/print.rs b/crates/ui-egui/src/dialogs/print.rs index 41c7cbc99..ea4f58d39 100644 --- a/crates/ui-egui/src/dialogs/print.rs +++ b/crates/ui-egui/src/dialogs/print.rs @@ -688,7 +688,7 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result) { for ink in inks { let name = ink["name"].as_str().unwrap_or_default(); let on = ink["print"].as_bool().unwrap_or(true); - if widgets::check(ui, "", on, true) { + if widgets::check_box(ui, name, on, true) { set_ink(d, name, "print", json!(!on)); } let spot = if ink["spot"] == true { format!(" ({})", tl!("spot")) } else { String::new() }; diff --git a/crates/ui-egui/src/dialogs/revolve.rs b/crates/ui-egui/src/dialogs/revolve.rs index 73f5cee55..28cb93353 100644 --- a/crates/ui-egui/src/dialogs/revolve.rs +++ b/crates/ui-egui/src/dialogs/revolve.rs @@ -371,10 +371,14 @@ fn number(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, range: (f64, }; let rail_min = from(range.0); let rail = rail_width(ui) + VALUE_W; - let slider = - widgets::color_slider(ui, ("revolve-slider", key), ((value - rail_min) / (rail_max - rail_min)).clamp(0.0, 1.0) as f32, rail, &|_| { - t.input_border - }); + let slider = widgets::color_slider_named( + ui, + ("revolve-slider", key), + name, + ((value - rail_min) / (rail_max - rail_min)).clamp(0.0, 1.0) as f32, + rail, + &|_| t.input_border, + ); if let (Some(n), _) = slider { next = Some(rail_min + f64::from(n) * (rail_max - rail_min)); } diff --git a/crates/ui-egui/src/dialogs/shortcut_import.rs b/crates/ui-egui/src/dialogs/shortcut_import.rs index bc5de6171..218cc218f 100644 --- a/crates/ui-egui/src/dialogs/shortcut_import.rs +++ b/crates/ui-egui/src/dialogs/shortcut_import.rs @@ -835,7 +835,7 @@ fn rows_section(ui: &mut egui::Ui, d: &mut Dialog, review: &mut Review, statuses let (status, chord) = statuses.get(i).cloned().unwrap_or((Status::NeedsChoice, None)); let Some(r) = review.rows.get_mut(i) else { continue }; ui.horizontal(|ui| { - if widgets::check(ui, "", r.on, true) { + if widgets::check_box(ui, &r.name, r.on, true) { r.on = !r.on; } let place = crate::i18n::fmt(tl!("Line {line}"), &[("line", &r.line.to_string())]); diff --git a/crates/ui-egui/src/dialogs/slices.rs b/crates/ui-egui/src/dialogs/slices.rs index da45a477f..723f5be62 100644 --- a/crates/ui-egui/src/dialogs/slices.rs +++ b/crates/ui-egui/src/dialogs/slices.rs @@ -141,7 +141,7 @@ fn divide_section(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, keys: ui.add_enabled_ui(enabled, |ui| { ui.indent(on, |ui| { ui.horizontal(|ui| { - if widgets::radio(ui, "", !by_size, enabled) { + if widgets::radio_button(ui, labels[1], !by_size, enabled) { d.fields.insert(mode.into(), json!("count")); } if let Some(n) = widgets::plain_field(ui, ("divide", count), d.f64(count, 2.0), "", 0, 50.0) { @@ -150,7 +150,7 @@ fn divide_section(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, keys: widgets::dim_label(ui, labels[1]); }); ui.horizontal(|ui| { - if widgets::radio(ui, "", by_size, enabled) { + if widgets::radio_button(ui, tl!("per slice"), by_size, enabled) { d.fields.insert(mode.into(), json!("size")); } form::length(ui, d, size, app.session.general_unit(), 70.0); @@ -244,6 +244,19 @@ mod tests { assert!(l["slices"].as_array().unwrap().iter().any(|s| s["options"]["kind"] == "image" && s["source"] == "object")); } + /// Screen readers name Divide Slices' radio buttons by the text after their fields. + #[test] + fn screen_readers_name_the_divide_radio_buttons() { + use crate::widgets::accessibility_tests::{role_and_state, tree}; + use egui::accesskit::{Role, Toggled}; + let mut app = app(); + app.run("object.slice.divide", json!({})).unwrap(); + set(&mut app, "divideRows", json!(true)); + let shown = tree(|ui| super::super::show(&mut app, ui.ctx())); + assert_eq!(role_and_state(&shown, "slices down, evenly spaced"), (Role::RadioButton, Some(Toggled::True))); + assert_eq!(role_and_state(&shown, "per slice"), (Role::RadioButton, Some(Toggled::False))); + } + #[test] fn divide_slices_cuts_by_count_or_size() { let mut app = app(); diff --git a/crates/ui-egui/src/dialogs/swatch_options.rs b/crates/ui-egui/src/dialogs/swatch_options.rs index 07674ef99..51bc62f5e 100644 --- a/crates/ui-egui/src/dialogs/swatch_options.rs +++ b/crates/ui-egui/src/dialogs/swatch_options.rs @@ -214,7 +214,7 @@ fn sliders(ui: &mut egui::Ui, d: &mut Dialog) -> bool { ui.add_sized(vec2(12.0, 22.0), egui::Label::new(egui::RichText::new(*lbl).size(12.5).color(t.text))); let v = comps.get(i).copied().unwrap_or(0.0); let track = |x: f32| crate::panels::c32(&track_color(mode, &comps, i, x)); - if let (Some(nv), _) = widgets::color_slider(ui, ("swatch-slider", i), mode.unit(i, v), 200.0, &track) { + if let (Some(nv), _) = widgets::color_slider_named(ui, ("swatch-slider", i), lbl, mode.unit(i, v), 200.0, &track) { let mut c2 = comps.clone(); c2[i] = mode.value_at(i, nv); new = Some((from_components(mode, &c2), c2)); diff --git a/crates/ui-egui/src/dialogs/transform_each.rs b/crates/ui-egui/src/dialogs/transform_each.rs index 51553051b..1d3f070cb 100644 --- a/crates/ui-egui/src/dialogs/transform_each.rs +++ b/crates/ui-egui/src/dialogs/transform_each.rs @@ -56,7 +56,7 @@ pub(super) fn sections(ui: &mut egui::Ui, d: &mut Dialog, units: vectorcraft_doc widgets::subheader(ui, tl!("Scale")); for (key, label) in axes("scaleH", "scaleV") { widgets::label_row(ui, label, LABEL_W, |ui| { - form::slider_rail(ui, d, key, 0.0..=200.0, &track); + form::slider_rail_named(ui, d, key, label, 0.0..=200.0, &track); if let Some(n) = widgets::range_field(ui, ("te", key), d.f64(key, 100.0), -MAX_SCALE..=MAX_SCALE, "%", 2, FIELD_W) { d.fields.insert(key.into(), json!(n)); } @@ -65,7 +65,7 @@ pub(super) fn sections(ui: &mut egui::Ui, d: &mut Dialog, units: vectorcraft_doc widgets::subheader(ui, tl!("Move")); for (key, label) in axes("moveH", "moveV") { widgets::label_row(ui, label, LABEL_W, |ui| { - form::slider_rail(ui, d, key, -1000.0..=1000.0, &track); + form::slider_rail_named(ui, d, key, label, -1000.0..=1000.0, &track); form::length(ui, d, key, units, FIELD_W); }); } diff --git a/crates/ui-egui/src/dialogs/variables.rs b/crates/ui-egui/src/dialogs/variables.rs index ee30d81a8..12ee10020 100644 --- a/crates/ui-egui/src/dialogs/variables.rs +++ b/crates/ui-egui/src/dialogs/variables.rs @@ -212,7 +212,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { VariableKind::Graph => { text_area(ui, d, &key, 220.0); } - VariableKind::Visibility => checkbox(ui, d, &key), + VariableKind::Visibility => checkbox(ui, d, &key, &v.name), } ui.end_row(); } @@ -232,10 +232,10 @@ fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> egui::Respo r } -/// A checkbox for `key`, written back as it is toggled. -fn checkbox(ui: &mut egui::Ui, d: &mut Dialog, key: &str) { +/// A checkbox for `key`, written back as it is toggled, which screen readers call `name`. +fn checkbox(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str) { let b = d.bool(key); - if widgets::check(ui, "", b, true) { + if widgets::check_box(ui, name, b, true) { d.fields.insert(key.into(), json!(!b)); } } diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index e3e8d7a51..5f93a3fd1 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -492,6 +492,7 @@ Color Model: Цветен модел: Color Models Цветни модели Color Picker Палитра с цвят + Color wheel Цветово колело Color Picker… Палитра с цвят… Color Reduction: Намаляване на цветовете: Color Settings Цветови настройки @@ -1175,6 +1176,10 @@ glyph {id} глиф {id} Glyphs Глифи Go to Artboard Към работна област + First Artboard Първа работна област + Previous Artboard Предишна работна област + Next Artboard Следваща работна област + Last Artboard Последна работна област Go To Link Към връзката GPL palettes keep solid colours only, as RGB. Палитрите GPL запазват само плътни цветове като RGB. GPU Performance Производителност на графичния процесор @@ -4284,6 +4289,7 @@ effect Soft Меко Reset all Нулиране на всичко Reset to default Връщане към по подразбиране Reset all Revolve settings to their defaults. Нулиране на всички настройки за въртене към техните стойности по подразбиране. + Step up or down Стъпка нагоре или надолу Drag values or labels. Double-click a value to type. Плъзнете стойности или етикети. Кликнете двукратно върху стойност за въвеждане. Drag rings to rotate. Shift snaps to 15°. Плъзнете пръстените за завъртане. Shift прилепва към 15°. Drag the light, or its rings. Shift snaps to 15°. Плъзнете светлината или нейните пръстени. Shift прилепва към 15°. diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 979004bf3..6ca2a3d9c 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -808,6 +808,7 @@ Reset all Obnovit vše Reset to default Obnovit výchozí hodnotu Reset all Revolve settings to their defaults. Obnovit výchozí hodnoty všech nastavení rotačního tělesa. + Step up or down O krok nahoru nebo dolů Drag values or labels. Double-click a value to type. Upravte hodnoty přetažením čísel nebo popisků. Dvojím kliknutím na hodnotu ji zadáte. Drag rings to rotate. Shift snaps to 15°. Otáčejte přetažením prstenců. Shift nastaví kroky po 15°. Drag the light, or its rings. Shift snaps to 15°. Přetáhněte světlo nebo jeho prstence. Shift nastaví kroky po 15°. @@ -855,6 +856,10 @@ lighting Light Světlo Export Keyboard Shortcuts… Exportovat klávesové zkratky… File Info Dialog Dialogové okno Informace o souboru Go to Artboard Přejít na pracovní plochu + First Artboard První pracovní plocha + Previous Artboard Předchozí pracovní plocha + Next Artboard Další pracovní plocha + Last Artboard Poslední pracovní plocha Graphic Style Options… Možnosti grafického stylu… Symbol Options Možnosti symbolu Symbol Options… Možnosti symbolu… @@ -1935,6 +1940,7 @@ lighting Light Světlo Variation Variace Color Models Barevné modely Color Picker Výběr barvy + Color wheel Barevné kolo Color Swatches Položky vzorníku Only Web Colors Jen webové barvy Original: click to restore Původní: kliknutím obnovíte diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index fdbab8116..9f7ee9465 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -492,6 +492,7 @@ Color Model: Farbmodell: Color Models Farbmodelle Color Picker Farbwähler + Color wheel Farbkreis Color Picker… Farbwähler… Color Reduction: Farbreduktion: Color Settings Farbeinstellungen @@ -1183,6 +1184,10 @@ glyph {id} Glyphe {id} Glyphs Glyphen Go to Artboard Gehe zu Zeichenfläche + First Artboard Erste Zeichenfläche + Previous Artboard Vorherige Zeichenfläche + Next Artboard Nächste Zeichenfläche + Last Artboard Letzte Zeichenfläche Go To Link Gehe zu Verknüpfung GPL palettes keep solid colours only, as RGB. GPL-Paletten enthalten nur einfarbige Farben, als RGB. GPU Performance GPU-Leistung @@ -4304,6 +4309,7 @@ theme Appearance Erscheinungsbild Drag rings to rotate. Shift snaps to 15°. Ziehen Sie die Ringe, um zu drehen. Mit Shift in 15°-Schritten. Drag the light, or its rings. Shift snaps to 15°. Ziehen Sie das Licht oder seine Ringe. Mit Shift in 15°-Schritten. Drag values or labels. Double-click a value to type. Ziehen Sie Werte oder Beschriftungen. Doppelklicken Sie auf einen Wert, um ihn einzugeben. + Step up or down Schrittweise erhöhen oder verringern Intensity Intensität Keep visible surfaces only when expanding Beim Umwandeln nur sichtbare Flächen behalten Lighting Beleuchtung diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 608c2749f..66977f54d 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -467,6 +467,7 @@ Color Model: Μοντέλο χρώματος: Color Models Μοντέλα χρώματος Color Picker Επιλογέας χρώματος + Color wheel Χρωματικός τροχός Color Picker… Επιλογέας χρώματος… Color Reduction: Μείωση χρωμάτων: Color Settings Ρυθμίσεις χρωμάτων @@ -1138,6 +1139,10 @@ glyph {id} γλύφος {id} Glyphs Γλύφοι Go to Artboard Μετάβαση στον πίνακα σχεδίασης + First Artboard Πρώτος πίνακας σχεδίασης + Previous Artboard Προηγούμενος πίνακας σχεδίασης + Next Artboard Επόμενος πίνακας σχεδίασης + Last Artboard Τελευταίος πίνακας σχεδίασης Go To Link Μετάβαση στη σύνδεση GPL palettes keep solid colours only, as RGB. Οι παλέτες GPL διατηρούν μόνο συμπαγή χρώματα σε RGB. GPU Performance Απόδοση GPU @@ -4236,6 +4241,7 @@ theme Appearance Εμφάνιση Reset all Επαναφορά όλων Reset to default Επαναφορά στην προεπιλογή Reset all Revolve settings to their defaults. Επαναφορά όλων των ρυθμίσεων περιστροφής γύρω από άξονα στις προεπιλογές. + Step up or down Ένα βήμα πάνω ή κάτω Drag values or labels. Double-click a value to type. Σύρετε τιμές ή ετικέτες. Κάντε διπλό κλικ σε μια τιμή για πληκτρολόγηση. Drag rings to rotate. Shift snaps to 15°. Σύρετε τους δακτυλίους για περιστροφή. Με Shift γίνεται προσκόλληση ανά 15°. Drag the light, or its rings. Shift snaps to 15°. Σύρετε την πηγή φωτός ή τους δακτυλίους της. Με Shift γίνεται προσκόλληση ανά 15°. diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 282e5dd11..1067afa8b 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -507,6 +507,7 @@ Color Model: Modelo de color: Color Models Modelos de color Color Picker Selector de color + Color wheel Rueda de color Color Picker… Selector de color… Color Reduction: Reducción de colores: Color Settings Ajustes de color @@ -1200,6 +1201,10 @@ glyph {id} glifo {id} Glyphs Glifos Go to Artboard Ir a mesa de trabajo + First Artboard Primera mesa de trabajo + Previous Artboard Mesa de trabajo anterior + Next Artboard Mesa de trabajo siguiente + Last Artboard Última mesa de trabajo Go To Link Ir al vínculo GPL palettes keep solid colours only, as RGB. Las paletas GPL solo conservan colores sólidos, en RGB. GPU Performance Rendimiento de GPU @@ -4351,6 +4356,7 @@ axis Both Ambos Reset all Restablecer todo Reset to default Restablecer valor predeterminado Reset all Revolve settings to their defaults. Restablece todos los ajustes de revolución a sus valores predeterminados. + Step up or down Subir o bajar un paso Drag values or labels. Double-click a value to type. Arrastra los valores o las etiquetas. Haz doble clic en un valor para escribir. Drag rings to rotate. Shift snaps to 15°. Arrastra los anillos para girar. Shift ajusta a pasos de 15°. Drag the light, or its rings. Shift snaps to 15°. Arrastra la luz o sus anillos. Shift ajusta a pasos de 15°. diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index a0fd4b8c1..c357563e5 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -789,6 +789,7 @@ Reset all Palauta kaikki Reset to default Palauta oletus Reset all Revolve settings to their defaults. Palauta kaikki pyöräytysasetukset oletusarvoihin. + Step up or down Askel ylös tai alas Drag values or labels. Double-click a value to type. Vedä arvoja tai nimiöitä. Kirjoita arvo kaksoisnapsauttamalla. Drag rings to rotate. Shift snaps to 15°. Kierrä vetämällä renkaita. Vaihto kohdistaa 15°:n välein. Drag the light, or its rings. Shift snaps to 15°. Vedä valoa tai sen renkaita. Vaihto kohdistaa 15°:n välein. @@ -834,6 +835,10 @@ lighting Light Valo Export Keyboard Shortcuts… Vie pikanäppäimet… File Info Dialog Tiedoston tiedot -valintaikkuna Go to Artboard Siirry piirtoalustaan + First Artboard Ensimmäinen piirtoalusta + Previous Artboard Edellinen piirtoalusta + Next Artboard Seuraava piirtoalusta + Last Artboard Viimeinen piirtoalusta Graphic Style Options… Grafiikkatyylin asetukset… Symbol Options Symbolin asetukset Symbol Options… Symbolin asetukset… @@ -1896,6 +1901,7 @@ lighting Light Valo Variation Vaihtelu Color Models Värijärjestelmät Color Picker Värinvalitsin + Color wheel Väriympyrä Color Swatches Värimallit Only Web Colors Vain verkkovärit Original: click to restore Alkuperäinen: palauta napsauttamalla diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 344b340dc..e74016c88 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -508,6 +508,7 @@ Color Model: Modèle colorimétrique : Color Models Modèles colorimétriques Color Picker Sélecteur de couleurs + Color wheel Roue chromatique Color Picker… Sélecteur de couleurs… Color Reduction: Réduction des couleurs : Color Settings Paramètres de couleur @@ -1199,6 +1200,10 @@ glyph {id} glyphe {id} Glyphs Glyphes Go to Artboard Atteindre le plan de travail + First Artboard Premier plan de travail + Previous Artboard Plan de travail précédent + Next Artboard Plan de travail suivant + Last Artboard Dernier plan de travail Go To Link Atteindre le lien GPL palettes keep solid colours only, as RGB. Les palettes GPL ne conservent que les couleurs unies, en RVB. GPU Performance Performances GPU @@ -4361,6 +4366,7 @@ effect Soft Doux Reset all Tout réinitialiser Reset to default Valeur par défaut Reset all Revolve settings to their defaults. Rétablir les valeurs par défaut de tous les réglages de révolution. + Step up or down Augmenter ou diminuer d’un cran Drag values or labels. Double-click a value to type. Faites glisser les valeurs ou les libellés. Double-cliquez sur une valeur pour la saisir. Drag rings to rotate. Shift snaps to 15°. Faites glisser les anneaux pour tourner. Maj contraint par pas de 15°. Drag the light, or its rings. Shift snaps to 15°. Faites glisser la lumière ou ses anneaux. Maj contraint par pas de 15°. diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index cf747808f..8e28c70a6 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -679,6 +679,7 @@ Color Model: Színmodell: Color Models Színmodellek Color Picker Színválasztó + Color wheel Színkör Color Picker… Színválasztó… Color Reduction: Színcsökkentés: Color Settings Színbeállítások @@ -1075,6 +1076,7 @@ Drag to move Húzással áthelyezhető Drag to move out of the set Húzással kivehető a csoportból Drag to the window's left edge to dock Dokkoláshoz húzd az ablak bal szélére + Step up or down Léptetés fel vagy le Drag values or labels. Double-click a value to type. Húzd az értékeket vagy a címkéket. Gépeléshez kattints duplán egy értékre. Draw Rajzolás Draw Behind Rajzolás mögé @@ -1502,6 +1504,10 @@ glyph {id} glifa {id} Glyphs Glifák Go to Artboard Ugrás rajztáblára + First Artboard Első rajztábla + Previous Artboard Előző rajztábla + Next Artboard Következő rajztábla + Last Artboard Utolsó rajztábla Go to Conflict Ugrás az ütközésre Go To Link Ugrás a csatoláshoz Good Jó diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index fe5f1a4dc..e781a57e6 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -507,6 +507,7 @@ Color Model: Modello colore: Color Models Modelli colore Color Picker Selettore colore + Color wheel Ruota dei colori Color Picker… Selettore colore… Color Reduction: Riduzione colori: Color Settings Impostazioni colore @@ -1199,6 +1200,10 @@ glyph {id} glifo {id} Glyphs Glifi Go to Artboard Vai alla tavola da disegno + First Artboard Prima tavola da disegno + Previous Artboard Tavola da disegno precedente + Next Artboard Tavola da disegno successiva + Last Artboard Ultima tavola da disegno Go To Link Vai al collegamento GPL palettes keep solid colours only, as RGB. Le palette GPL conservano solo i colori pieni, in RGB. GPU Performance Prestazioni GPU @@ -4356,6 +4361,7 @@ effect Regular Normale Reset all Ripristina tutto Reset to default Ripristina valore predefinito Reset all Revolve settings to their defaults. Ripristina tutte le impostazioni di rivoluzione ai valori predefiniti. + Step up or down Aumenta o riduci di un passo Drag values or labels. Double-click a value to type. Trascina i valori o le etichette. Fai doppio clic su un valore per digitarlo. Drag rings to rotate. Shift snaps to 15°. Trascina gli anelli per ruotare. Shift usa incrementi di 15°. Drag the light, or its rings. Shift snaps to 15°. Trascina la luce o i suoi anelli. Shift usa incrementi di 15°. diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index af96e8078..e925da051 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1051,6 +1051,7 @@ Color Model: カラーモデル: Color Models カラーモデル Color Picker カラーピッカー + Color wheel カラーホイール Color Picker… カラーピッカー… Color Reduction: カラー減色: Color Settings カラー設定 @@ -1513,6 +1514,10 @@ Global Process Color グローバルプロセスカラー Go To Link リンク元へ移動 Go to Artboard アートボードへ移動 + First Artboard 最初のアートボード + Previous Artboard 前のアートボード + Next Artboard 次のアートボード + Last Artboard 最後のアートボード Gradient Fill: click to apply, drag onto art グラデーションの塗り: クリックで適用、アートにドラッグ Gradient Mesh グラデーションメッシュ Gradient Tool グラデーションツール @@ -4396,6 +4401,7 @@ effect Vertical 垂直 Reset all すべてリセット Reset to default 初期値に戻す Reset all Revolve settings to their defaults. 回転体のすべての設定を初期値に戻します。 + Step up or down 1 段階ずつ増減 Drag values or labels. Double-click a value to type. 数値やラベルをドラッグして調整。数値をダブルクリックすると入力できます。 Drag rings to rotate. Shift snaps to 15°. リングをドラッグして回転。Shiftで15°刻みに調整。 Drag the light, or its rings. Shift snaps to 15°. ライトやリングをドラッグ。Shiftで15°刻みに調整。 diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 4268d7378..c22818da9 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1046,6 +1046,7 @@ Color Model: 색상 모델: Color Models 색상 모델 Color Picker 색상 피커 + Color wheel 색상환 Color Picker… 색상 피커… Color Reduction: 색상 감소: Color Settings 색상 설정 @@ -1508,6 +1509,10 @@ Global Process Color 전역 프로세스 색상 Go To Link 링크로 이동 Go to Artboard 아트보드로 이동 + First Artboard 첫 아트보드 + Previous Artboard 이전 아트보드 + Next Artboard 다음 아트보드 + Last Artboard 마지막 아트보드 Gradient Fill: click to apply, drag onto art 그레이디언트 칠: 클릭하여 적용, 아트웍에 끌어 놓기 Gradient Mesh 그레이디언트 메시 Gradient Tool 그레이디언트 도구 @@ -4322,6 +4327,7 @@ effect Vertical 세로 Reset all 모두 재설정 Reset to default 기본값으로 재설정 Reset all Revolve settings to their defaults. 모든 회전체 설정을 기본값으로 재설정합니다. + Step up or down 한 단계 올리기 또는 내리기 Drag values or labels. Double-click a value to type. 값이나 레이블을 드래그하세요. 값을 더블클릭하면 입력할 수 있습니다. Drag rings to rotate. Shift snaps to 15°. 링을 드래그해 회전하세요. Shift를 누르면 15°씩 스냅됩니다. Drag the light, or its rings. Shift snaps to 15°. 조명이나 링을 드래그하세요. Shift를 누르면 15°씩 스냅됩니다. diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index ba1a49344..42801725d 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -747,6 +747,7 @@ Reset all Resetuj wszystko Reset to default Przywróć domyślne Reset all Revolve settings to their defaults. Przywróć domyślne wartości wszystkich ustawień obrotu. + Step up or down Zwiększ lub zmniejsz o krok Drag values or labels. Double-click a value to type. Przeciągaj wartości lub etykiety. Kliknij dwukrotnie wartość, aby ją wpisać. Drag rings to rotate. Shift snaps to 15°. Przeciągaj pierścienie, aby obracać. Shift przyciąga co 15°. Drag the light, or its rings. Shift snaps to 15°. Przeciągaj światło lub jego pierścienie. Shift przyciąga co 15°. @@ -792,6 +793,10 @@ lighting Light Światło Export Keyboard Shortcuts… Eksportuj skróty klawiaturowe… File Info Dialog Okno informacji o pliku Go to Artboard Przejdź do obszaru kompozycji + First Artboard Pierwszy obszar kompozycji + Previous Artboard Poprzedni obszar kompozycji + Next Artboard Następny obszar kompozycji + Last Artboard Ostatni obszar kompozycji Graphic Style Options… Opcje stylu graficznego… Symbol Options Opcje symbolu Symbol Options… Opcje symbolu… @@ -1843,6 +1848,7 @@ lighting Light Światło Variation: Odchylenie: Color Models Modele kolorów Color Picker Próbnik kolorów + Color wheel Koło kolorów Color Swatches Próbki kolorów Only Web Colors Tylko kolory WWW Original: click to restore Oryginał: kliknij, aby przywrócić diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 32bf7d118..ea126556b 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -463,6 +463,7 @@ Color Model: Modelo de Cor: Color Models Modelos de Cor Color Picker Seletor de Cores + Color wheel Roda de cores Color Picker… Seletor de Cores… Color Reduction: Redução de Cores: Color Settings Configurações de Cor @@ -1055,6 +1056,10 @@ Glyphs Glifos Go To Link Ir para o Link Go to Artboard Ir para Mesa de Trabalho + First Artboard Primeira mesa de trabalho + Previous Artboard Mesa de trabalho anterior + Next Artboard Próxima mesa de trabalho + Last Artboard Última mesa de trabalho Gradient Gradiente Gradient Fill: click to apply, drag onto art Preenchimento de Gradiente: clique para aplicar, arraste sobre a arte Gradient Finishes Finalizações de Gradiente @@ -2923,6 +2928,7 @@ imageTrace Outline Contorno Reset all Redefinir tudo Reset to default Restaurar padrão Reset all Revolve settings to their defaults. Restaurar os valores padrão de todas as configurações de revolução. + Step up or down Aumentar ou diminuir um passo Drag values or labels. Double-click a value to type. Arraste os valores ou rótulos. Clique duas vezes em um valor para digitá-lo. Drag rings to rotate. Shift snaps to 15°. Arraste os anéis para girar. Shift ajusta em incrementos de 15°. Drag the light, or its rings. Shift snaps to 15°. Arraste a luz ou seus anéis. Shift ajusta em incrementos de 15°. diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 8d1287235..c9c9a4055 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -505,6 +505,7 @@ Color Model: Цветовая модель: Color Models Цветовые модели Color Picker Палитра цветов + Color wheel Цветовой круг Color Picker… Палитра цветов… Color Reduction: Сокращение числа цветов: Color Settings Настройки цвета @@ -1197,6 +1198,10 @@ glyph {id} глиф {id} Glyphs Глифы Go to Artboard Перейти к монтажной области + First Artboard Первая монтажная область + Previous Artboard Предыдущая монтажная область + Next Artboard Следующая монтажная область + Last Artboard Последняя монтажная область Go To Link Перейти по ссылке GPL palettes keep solid colours only, as RGB. Палитры GPL сохраняют только сплошные цвета в RGB. GPU Performance Производительность GPU @@ -4352,6 +4357,7 @@ axis Both Оба Reset all Сбросить всё Reset to default Вернуть значение по умолчанию Reset all Revolve settings to their defaults. Возвращает все настройки вращения профиля к значениям по умолчанию. + Step up or down Шаг вверх или вниз Drag values or labels. Double-click a value to type. Перетаскивайте значения или подписи. Для ввода дважды щёлкните значение. Drag rings to rotate. Shift snaps to 15°. Перетаскивайте кольца для вращения. Shift задаёт шаг 15°. Drag the light, or its rings. Shift snaps to 15°. Перетаскивайте источник света или кольца. Shift задаёт шаг 15°. diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 713a977d0..f182c945a 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -494,6 +494,7 @@ Color Model: Färgmodell: Color Models Färgmodeller Color Picker Färgväljare + Color wheel Färghjul Color Picker… Färgväljare… Color Reduction: Färgreducering: Color Settings Färginställningar @@ -1177,6 +1178,10 @@ glyph {id} glyf {id} Glyphs Glyfer Go to Artboard Gå till rityta + First Artboard Första ritytan + Previous Artboard Föregående rityta + Next Artboard Nästa rityta + Last Artboard Sista ritytan Go To Link Gå till länk GPL palettes keep solid colours only, as RGB. GPL-paletter sparar bara enfärgade färger, som RGB. GPU Performance GPU-prestanda @@ -4283,6 +4288,7 @@ effect Soft Mjuk Reset all Återställ alla Reset to default Återställ till standard Reset all Revolve settings to their defaults. Återställ alla inställningar för Rotera runt axel till standardvärdena. + Step up or down Ett steg upp eller ned Drag values or labels. Double-click a value to type. Dra i värden eller etiketter. Dubbelklicka på ett värde för att skriva. Drag rings to rotate. Shift snaps to 15°. Dra i ringarna för att rotera. Shift fäster i steg om 15°. Drag the light, or its rings. Shift snaps to 15°. Dra i ljuset eller dess ringar. Shift fäster i steg om 15°. diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 5d64483ec..a5299733a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -501,6 +501,7 @@ Color Model: Колірна модель: Color Models Колірні моделі Color Picker Палітра кольорів + Color wheel Колірне коло Color Picker… Палітра кольорів… Color Reduction: Скорочення кількості кольорів: Color Settings Налаштування кольору @@ -1193,6 +1194,10 @@ glyph {id} гліф {id} Glyphs Гліфи Go to Artboard Перейти до монтажної області + First Artboard Перша монтажна область + Previous Artboard Попередня монтажна область + Next Artboard Наступна монтажна область + Last Artboard Остання монтажна область Go To Link Перейти за посиланням GPL palettes keep solid colours only, as RGB. Палітри GPL зберігають лише суцільні кольори RGB. GPU Performance Продуктивність GPU @@ -4336,6 +4341,7 @@ axis Both Обидві Reset all Скинути все Reset to default Відновити типове значення Reset all Revolve settings to their defaults. Відновити типові значення всіх параметрів обертання. + Step up or down Крок угору або вниз Drag values or labels. Double-click a value to type. Перетягуйте значення або підписи. Двічі клацніть значення, щоб увести його. Drag rings to rotate. Shift snaps to 15°. Перетягуйте кільця для обертання. Shift задає крок 15°. Drag the light, or its rings. Shift snaps to 15°. Перетягуйте джерело світла або його кільця. Shift задає крок 15°. diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index dd552e948..149cb4702 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -592,6 +592,7 @@ Color Model: Mô hình màu: Color Models Các mô hình màu Color Picker Bộ chọn màu + Color wheel Bánh xe màu Color Picker… Bộ chọn màu… Color Reduction: Giảm màu: Color Settings Cài đặt màu @@ -960,6 +961,7 @@ Drag to move Kéo để di chuyển Drag to move out of the set Kéo để tách khỏi bộ Drag to the window's left edge to dock Kéo đến cạnh trái cửa sổ để neo + Step up or down Tăng hoặc giảm một bước Drag values or labels. Double-click a value to type. Kéo giá trị hoặc nhãn. Bấm đúp vào giá trị để nhập. Draw Vẽ Draw Behind Vẽ phía sau @@ -1377,6 +1379,10 @@ glyph {id} Glyph {id} Glyphs Glyph Go to Artboard Đến bảng vẽ + First Artboard Bảng vẽ đầu tiên + Previous Artboard Bảng vẽ trước + Next Artboard Bảng vẽ tiếp theo + Last Artboard Bảng vẽ cuối cùng Go To Link Đến liên kết Good Tốt GPL palettes keep solid colours only, as RGB. Bảng màu GPL chỉ lưu màu đặc theo RGB. diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 251768913..c1e8299d3 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -462,6 +462,7 @@ Color Model: 颜色模型: Color Models 颜色模型 Color Picker 拾色器 + Color wheel 色轮 Color Picker… 颜色选择器… Color Reduction: 色彩缩减: Color Settings 颜色设置 @@ -1146,6 +1147,10 @@ glyph {id} 字符 {id} Glyphs 字形 Go to Artboard 前往画板 + First Artboard 第一个画板 + Previous Artboard 上一个画板 + Next Artboard 下一个画板 + Last Artboard 最后一个画板 Go To Link 转到链接 GPL palettes keep solid colours only, as RGB. GPL 调板仅保留为 RGB 的纯色。 GPU Performance GPU 性能 @@ -3930,6 +3935,7 @@ effect Vertical 垂直 Reset all 全部重置 Reset to default 重置为默认值 Reset all Revolve settings to their defaults. 将所有旋转成形设置重置为默认值。 + Step up or down 逐级增减 Drag values or labels. Double-click a value to type. 拖动数值或标签。双击数值即可输入。 Drag rings to rotate. Shift snaps to 15°. 拖动圆环旋转。按住 Shift 以 15° 为步长调整。 Drag the light, or its rings. Shift snaps to 15°. 拖动光源或圆环。按住 Shift 以 15° 为步长调整。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 23bdde637..696f6fc26 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -476,6 +476,7 @@ Color Model: 色彩模型: Color Models 色彩模型 Color Picker 顏色選擇器 + Color wheel 色輪 Color Picker… 顏色選擇器… Color Reduction: 色彩縮減: Color Settings 色彩設定 @@ -1170,6 +1171,10 @@ glyph {id} 字符 {id} Glyphs 字符 Go to Artboard 前往工作區域 + First Artboard 第一個工作區域 + Previous Artboard 上一個工作區域 + Next Artboard 下一個工作區域 + Last Artboard 最後一個工作區域 Go To Link 前往連結 GPL palettes keep solid colours only, as RGB. GPL 調色盤僅保留純色,並以 RGB 儲存。 GPU Performance GPU 效能 @@ -3714,6 +3719,7 @@ effect Soft 柔和 Reset all 全部重設 Reset to default 重設為預設值 Reset all Revolve settings to their defaults. 將所有旋轉成形設定重設為預設值。 + Step up or down 逐級增減 Drag values or labels. Double-click a value to type. 拖曳數值或標籤。按兩下數值即可輸入。 Drag rings to rotate. Shift snaps to 15°. 拖曳圓環旋轉。按住 Shift 以 15° 為間距調整。 Drag the light, or its rings. Shift snaps to 15°. 拖曳光源或圓環。按住 Shift 以 15° 為間距調整。 diff --git a/crates/ui-egui/src/panels/align.rs b/crates/ui-egui/src/panels/align.rs index 2902fbad9..c675bce54 100644 --- a/crates/ui-egui/src/panels/align.rs +++ b/crates/ui-egui/src/panels/align.rs @@ -250,9 +250,12 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { #[cfg(test)] mod tests { + use egui::accesskit::{Role, Toggled}; + use super::super::tests_appearance::{click, frame_events, text_rect}; use super::*; use crate::toolbar::tests::widget_rects; + use crate::widgets::accessibility_tests::{role_and_state, tree}; #[test] fn single_object_aligns_to_artboard() { @@ -466,6 +469,20 @@ mod tests { assert_eq!(align_to(&app), AlignTo::Selection, "the stored choice"); } + /// Screen readers name the align buttons, with no pressed state, and the Align To buttons, the + /// chosen one pressed. + #[test] + fn screen_readers_name_the_buttons_and_the_align_to_choice() { + let (mut app, _, _) = back_and_front(); + let tree = tree(|ui| show(&mut app, ui)); + for (_, label, _, _) in ALIGN.iter().chain(DISTRIBUTE.iter()) { + assert_eq!(role_and_state(&tree, label), (Role::Button, None), "{label}"); + } + assert_eq!(role_and_state(&tree, "Align to Selection"), (Role::Button, Some(Toggled::True))); + assert_eq!(role_and_state(&tree, "Align to Key Object"), (Role::Button, None)); + assert_eq!(role_and_state(&tree, "Align to Artboard"), (Role::Button, None)); + } + /// #789: the Control bar's and the Properties panel's align buttons follow Align To too. #[test] fn the_other_align_buttons_align_to_the_align_to_choice() { diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs index a6b5a9d62..2d3d94b2b 100644 --- a/crates/ui-egui/src/panels/appearance.rs +++ b/crates/ui-egui/src/panels/appearance.rs @@ -761,7 +761,7 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option, k: } match v { Value::Bool(b) => { - if widgets::check(ui, "", *b, true) { + if widgets::check_box(ui, label, *b, true) { change = Some((key.clone(), json!(!b))); } } diff --git a/crates/ui-egui/src/panels/color.rs b/crates/ui-egui/src/panels/color.rs index 58b915f4b..61349e934 100644 --- a/crates/ui-egui/src/panels/color.rs +++ b/crates/ui-egui/src/panels/color.rs @@ -498,7 +498,7 @@ fn tint_rows(ui: &mut Ui, tint: &Tint, slider_w: f32, field_w: f32) -> Option<(f ui.horizontal(|ui| { ui.add_sized(vec2(12.0, 22.0), egui::Label::new(egui::RichText::new("T").size(12.5).color(t.text))); let track = |x: f32| to32(&tint.base.tinted(x)); - let (nv, phase) = widgets::color_slider(ui, ("color-slider", "tint"), tint.tint, slider_w, &track); + let (nv, phase) = widgets::color_slider_named(ui, ("color-slider", "tint"), "T", tint.tint, slider_w, &track); let field = widgets::plain_field(ui, ("color-field", "tint"), (tint.tint * 100.0).round() as f64, "%", 0, field_w); match (nv, field) { (Some(v), _) => Some(((v * 100.0).round() / 100.0, phase)), @@ -613,9 +613,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let track = |x: f32| to32(&track_color(mode, &comps, i, x)); let grey = |_x: f32| t.input; let (nv, phase) = if enabled { - widgets::color_slider(ui, ("color-slider", i), mode.unit(i, v), slider_w, &track) + widgets::color_slider_named(ui, ("color-slider", i), lbl, mode.unit(i, v), slider_w, &track) } else { - widgets::color_slider(ui, ("color-slider", i), 0.0, slider_w, &grey) + widgets::color_slider_named(ui, ("color-slider", i), lbl, 0.0, slider_w, &grey) }; if let Some(nv) = nv && enabled diff --git a/crates/ui-egui/src/panels/color_guide.rs b/crates/ui-egui/src/panels/color_guide.rs index c9c8a478c..8de5ff1eb 100644 --- a/crates/ui-egui/src/panels/color_guide.rs +++ b/crates/ui-egui/src/panels/color_guide.rs @@ -216,7 +216,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { None => tl!("Limit colors to swatch library").into(), }; // The tip is translated around the library's name, not looked up whole. - let lr = widgets::icon_button(ui, "library", "", limit.is_some(), 24.0).on_hover_text(tip); + let lr = widgets::icon_button(ui, "library", "", limit.is_some(), 24.0); + lr.widget_info(|| widgets::icon_button_info(ui.is_enabled(), limit.is_some(), &tip)); + let lr = lr.on_hover_text(tip); egui::Popup::menu(&lr).show(|ui| limit_menu(app, ui)); ui.add_space((ui.available_width() - 3.0 * 28.0).max(0.0)); // Recolor Artwork with the harmony colours as the new colours: the selected art's, or @@ -278,6 +280,20 @@ mod tests { out.textures_delta.clear(); } + /// Screen readers name the library button by its tool tip, pressed while a library limits the + /// colors. + #[test] + fn screen_readers_name_the_limit_button() { + use crate::widgets::accessibility_tests::{role_and_state, tree}; + use egui::accesskit::{Role, Toggled}; + let mut app = app(); + let shown = tree(|ui| show(&mut app, ui)); + assert_eq!(role_and_state(&shown, "Limit colors to swatch library"), (Role::Button, None)); + app.run("ui.colorGuideLimit", json!({"library": "Earth Tones"})).unwrap(); + let shown = tree(|ui| show(&mut app, ui)); + assert_eq!(role_and_state(&shown, "Limit colors to swatch library: Earth Tones"), (Role::Button, Some(Toggled::True))); + } + #[test] fn base_stays_until_set_and_the_grid_follows_the_options() { let mut app = app(); diff --git a/crates/ui-egui/src/panels/color_themes.rs b/crates/ui-egui/src/panels/color_themes.rs index 751f8dfd8..acc2fa8bb 100644 --- a/crates/ui-egui/src/panels/color_themes.rs +++ b/crates/ui-egui/src/panels/color_themes.rs @@ -212,7 +212,9 @@ fn create(app: &mut VectorcraftApp, ui: &mut Ui) { None => tl!("Save theme to My Themes").into(), }; // The tip is translated around the theme's name, not looked up whole. - if widgets::icon_button(ui, "save", "", false, 24.0).on_hover_text(tip).clicked() { + let resp = widgets::icon_button(ui, "save", "", false, 24.0); + resp.widget_info(|| widgets::icon_button_info(ui.is_enabled(), false, &tip)); + if resp.on_hover_text(tip).clicked() { save(app, &ctx); } let doc = app.session.active().is_some(); @@ -368,6 +370,15 @@ mod tests { &app.session.prefs.color_themes } + /// Screen readers name the Save button by its tool tip. + #[test] + fn screen_readers_name_the_save_button() { + use crate::widgets::accessibility_tests::{role_and_state, tree}; + let mut app = app(); + let shown = tree(|ui| show(&mut app, ui)); + assert_eq!(role_and_state(&shown, "Save theme to My Themes"), (egui::accesskit::Role::Button, None)); + } + #[test] fn create_save_edit_and_resave_a_theme() { let mut app = app(); diff --git a/crates/ui-egui/src/panels/image_trace.rs b/crates/ui-egui/src/panels/image_trace.rs index 32e667881..ae02d1cbf 100644 --- a/crates/ui-egui/src/panels/image_trace.rs +++ b/crates/ui-egui/src/panels/image_trace.rs @@ -174,11 +174,10 @@ pub(crate) const TRACE_BUTTON_W: f32 = 104.0; /// The Image Trace button for a selected image (Control bar, Properties): a click traces it with /// the Default preset, its arrow lists the presets to trace it with. pub(crate) fn trace_button(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) { - let (main, arrow) = widgets::split_button(ui, tl!("Image Trace"), width); + let (main, arrow) = widgets::split_button_named(ui, tl!("Image Trace"), tl!("Tracing Presets"), width); if main.on_hover_text(tl!("Trace the image with the Default preset")).clicked() { trace_with(app, "Default"); } - let arrow = arrow.on_hover_text(tl!("Tracing Presets")); if let Some(p) = egui::Popup::menu(&arrow).show(|ui| preset_list(app, ui, "")).and_then(|r| r.inner) { trace_with(app, &p); } diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index e76596486..984b8385c 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -385,13 +385,13 @@ pub(crate) fn paint_chip(app: &mut VectorcraftApp, ui: &mut Ui, stroke: bool, si let (fill, stroke_paint) = current_paints(app); let mixed = mixed_paints(app, ui.ctx()); let (paint, mixed) = if stroke { (stroke_paint, mixed.1) } else { (fill, mixed.0) }; - let resp = crate::widgets::chip_button(ui, size, chevron, |ui, chip| { - crate::widgets::proxy_chip(ui, chip, &paint, mixed, stroke.then_some(size * 0.25)); - }); - let resp = resp.on_hover_text(if stroke { - "Stroke: click for swatches, Shift-click for the color mixer" + let tip = if stroke { + tl!("Stroke: click for swatches, Shift-click for the color mixer") } else { - "Fill: click for swatches, Shift-click for the color mixer" + tl!("Fill: click for swatches, Shift-click for the color mixer") + }; + let resp = crate::widgets::chip_button_named(ui, size, chevron, tip, |ui, chip| { + crate::widgets::proxy_chip(ui, chip, &paint, mixed, stroke.then_some(size * 0.25)); }); if resp.clicked() { app.run("paint.toggleActive", json!({ "fill": !stroke })).ok(); diff --git a/crates/ui-egui/src/panels/navigator.rs b/crates/ui-egui/src/panels/navigator.rs index 4af13e7a4..fa56be3cf 100644 --- a/crates/ui-egui/src/panels/navigator.rs +++ b/crates/ui-egui/src/panels/navigator.rs @@ -263,7 +263,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } let sw = (ui.available_width() - 30.0).max(40.0); let track = |_: f32| t.input_border; - if let (Some(v), phase) = widgets::color_slider(ui, "nav-slider", zoom_to_slider(zoom) as f32, sw, &track) + if let (Some(v), phase) = widgets::color_slider_named(ui, "nav-slider", tl!("Zoom"), zoom_to_slider(zoom) as f32, sw, &track) && phase != widgets::Live::Idle { z = Some(slider_to_zoom(v as f64)); diff --git a/crates/ui-egui/src/panels/pathfinder.rs b/crates/ui-egui/src/panels/pathfinder.rs index 425772367..440b4053b 100644 --- a/crates/ui-egui/src/panels/pathfinder.rs +++ b/crates/ui-egui/src/panels/pathfinder.rs @@ -100,6 +100,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { #[cfg(test)] mod tests { use super::*; + use crate::widgets::accessibility_tests::{named, tree}; use vectorcraft_engine::doc::{NodeId, NodeKind}; /// #1635: a stroked object expanded inside a group leaves a group holding one group (its fill @@ -125,4 +126,20 @@ mod tests { let b = united.geometric_bounds().unwrap(); assert_eq!((b.x0, b.y0, b.x1, b.y1), (5.0, 5.0, 65.0, 65.0), "the stroke's outer edge"); } + + /// Screen readers name each Shape Mode and Pathfinder button, enabled for two selected objects. + #[test] + fn screen_readers_name_the_buttons() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let a = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap()["id"].clone(); + let b = app.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 50, "height": 50})).unwrap()["id"].clone(); + app.run("select.set", json!({"ids": [a, b]})).unwrap(); + let tree = tree(|ui| show(&mut app, ui)); + for (_, label, _) in SHAPE_MODES.into_iter().chain(PATHFINDERS) { + let node = named(&tree, label); + assert_eq!(node.role(), egui::accesskit::Role::Button, "{label}"); + assert!(!node.is_disabled(), "{label}"); + } + } } diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index e46b86498..4c9235cb1 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -161,6 +161,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option widgets::gradient_chip(ui, r, &app.session.last_gradient), _ => widgets::paint_chip(ui, r, &Paint::None), } + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(tip))); if resp.on_hover_text(tl!(tip)).clicked() { clicked = Some(cmd); } @@ -600,6 +604,7 @@ fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating let x = egui::Rect::from_min_size(bar.min, vec2(BAR, CLOSE)); let xr = ui.interact(x, egui::Id::new(("tool-flyout-close", key)), Sense::click()); icons::paint(ui, "x", x.shrink2(vec2(1.0, 3.0)), if xr.hovered() { t.text_strong } else { t.text }); + xr.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!("Put back in the toolbar"))); out.close = xr.on_hover_text(tl!("Put back in the toolbar")).clicked(); egui::Rect::from_min_max(bar.min + vec2(0.0, CLOSE), bar.max) } else { @@ -611,11 +616,12 @@ fn flyout_body(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String], floating let x = grip.center().x - 2.0 + k as f32 * 2.0; ui.painter().line_segment([pos2(x, grip.center().y - half), pos2(x, grip.center().y + half)], Stroke::new(0.6, t.text_disabled)); } + let tip = if floating { tl!("Drag to move") } else { tl!("Drag to float as a toolbar") }; + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tip)); let resp = if resp.dragged() { ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing); resp } else { - let tip = if floating { tl!("Drag to move") } else { tl!("Drag to float as a toolbar") }; resp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(tip) }; out.bar = Some(resp); @@ -647,6 +653,7 @@ fn tool_rows(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String]) -> Option< if let Some(sc) = shortcut { ui.painter().galley(pos2(r.right() - ROW_END - sc.size().x, r.center().y - sc.size().y / 2.0), sc, color); } + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), is_active, tip(tool))); if resp.clicked() { chosen = Some(tool.id); } @@ -700,6 +707,7 @@ fn tool_strip(ui: &mut Ui, t: &Tokens, active: &str, tools: &[String]) -> Option } let ir = egui::Rect::from_center_size(r.center(), vec2(18.0, 18.0)); icons::paint(ui, icons::tool_icon(tool.icon), ir, if active == tool.id { t.text_strong } else { t.icon }); + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), active == tool.id, tip(tool))); if resp.on_hover_text(tip(tool)).clicked() { chosen = Some(tool.id); } @@ -840,10 +848,12 @@ fn puppet_warp_options(app: &mut VectorcraftApp, ui: &mut Ui) { #[cfg(test)] pub(crate) mod tests { + use egui::accesskit::{Role, Toggled}; use egui::{Event, PointerButton, Pos2}; use vectorcraft_engine::Session; use super::*; + use crate::widgets::accessibility_tests::{role_and_state, tree}; /// One headless frame of the toolbar; returns the tool buttons' rects, top to bottom. fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64, events: Vec) -> Vec { @@ -929,6 +939,22 @@ pub(crate) mod tests { } } + /// Screen readers name each tool button by its tool tip and say which tool is active, and name + /// the title bar's column toggle and grip and the paint buttons under the tools. + #[test] + fn screen_readers_name_the_tool_buttons_and_the_active_tool() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); + app.select_tool("pen"); + let tree = tree(|ui| show(&mut app, ui)); + let name = |id| tool_info(id).map(tip).unwrap_or_default(); + assert_eq!(role_and_state(&tree, &name("pen")), (Role::Button, Some(Toggled::True))); + assert_eq!(role_and_state(&tree, &name("selection")), (Role::Button, Some(Toggled::False))); + for label in ["Toggle single/double column", "Drag to float the toolbar", "Color (,)", "Gradient (.)", "None (/)"] { + assert_eq!(role_and_state(&tree, label), (Role::Button, None), "{label}"); + } + } + /// #812: double-clicking the Artboard tool opens Artboard Options of the active artboard, a /// graph tool Graph Type for the selected graph, and the Magic Wand tool its panel. #[test] @@ -1564,4 +1590,31 @@ pub(crate) mod tests { app.session.prefs.show_tool_tips = true; assert!(hovered_texts(&mut app, &ctx, 30.0, star).contains(&tip), "on again"); } + + /// Screen readers name a flyout's rows and a floating strip's buttons by their tools' tool tips, + /// with the active tool's pressed, and name the strip's close box and both tear-off bars. + #[test] + fn screen_readers_name_a_flyouts_rows_and_a_floating_strips_buttons() { + let name = |id| tool_info(id).map(tip).unwrap_or_default(); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); + let group: Vec = ["rectangle", "roundedRectangle", "ellipse"].map(String::from).to_vec(); + let flyout = tree(|ui| { + app.ui.flyout = Some(0); + ui.data_mut(|d| d.insert_temp(egui::Id::new("flyout-tools"), group.clone())); + show(&mut app, ui); + }); + assert_eq!(role_and_state(&flyout, &name("roundedRectangle")), (Role::Button, Some(Toggled::False))); + assert_eq!(role_and_state(&flyout, &name("ellipse")), (Role::Button, Some(Toggled::False))); + assert_eq!(role_and_state(&flyout, "Drag to float as a toolbar"), (Role::Button, None)); + app.ui.flyout = None; + app.select_tool("star"); + let tools: Vec = ["polygon", "star"].map(String::from).to_vec(); + app.ui.floating_flyouts.push(crate::state::FloatingFlyout { tools, pos: [150.0, 600.0] }); + let strip = tree(|ui| show(&mut app, ui)); + assert_eq!(role_and_state(&strip, &name("polygon")), (Role::Button, Some(Toggled::False))); + assert_eq!(role_and_state(&strip, &name("star")), (Role::Button, Some(Toggled::True))); + assert_eq!(role_and_state(&strip, "Put back in the toolbar"), (Role::Button, None)); + assert_eq!(role_and_state(&strip, "Drag to move"), (Role::Button, None)); + } } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 3d0096cd5..0dd7347e8 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -28,6 +28,7 @@ pub fn paint_icon_button(ui: &mut Ui, resp: Response, icon: &str, tip: &str, sel ui.painter().rect_filled(rect, CornerRadius::same(5), bg); let pad = (size * 0.2).round(); icons::paint(ui, icon, rect.shrink(pad), if selected { t.text_strong } else { t.icon }); + resp.widget_info(|| icon_button_info(ui.is_enabled(), selected, tl!(tip))); if !tip.is_empty() { resp.on_hover_text(tl!(tip)) } else { resp } } @@ -39,6 +40,7 @@ pub fn flat_button(ui: &mut Ui, text: &str, width: f32) -> Response { ui.painter().rect_filled(rect, CornerRadius::same(5), t.button); flat_fill(ui, rect, &resp); ui.painter().with_clip_rect(rect).text(rect.center(), egui::Align2::CENTER_CENTER, tl!(text), egui::FontId::proportional(12.5), t.text_strong); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(text))); resp } @@ -48,6 +50,12 @@ pub const SPLIT_ARROW: f32 = 18.0; /// A [`flat_button`] `width` wide whose right end is a menu arrow (Image Trace ▾) → the /// responses of the button and of its arrow. pub fn split_button(ui: &mut Ui, text: &str, width: f32) -> (Response, Response) { + split_button_named(ui, text, "", width) +} + +/// [`split_button`] whose arrow has the tool tip `arrow_tip`, which screen readers also call it +/// (none when empty). +pub fn split_button_named(ui: &mut Ui, text: &str, arrow_tip: &str, width: f32) -> (Response, Response) { let t = Tokens::get(ui.ctx()); let (rect, _) = ui.allocate_exact_size(vec2(width, 24.0), Sense::hover()); let rect = rect.shrink2(vec2(0.0, 0.5)); @@ -68,7 +76,12 @@ pub fn split_button(ui: &mut Ui, text: &str, width: f32) -> (Response, Response) t.text_strong, ); icons::paint(ui, "chevron-down", Rect::from_center_size(arrow_rect.center(), vec2(12.0, 12.0)), t.text_dim); - (main, arrow) + main.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(text))); + if arrow_tip.is_empty() { + return (main, arrow); + } + arrow.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(arrow_tip))); + (main, arrow.on_hover_text(tl!(arrow_tip))) } /// The pressed or hovered fill of a flat button part. @@ -103,6 +116,7 @@ pub fn primary_button_enabled(ui: &mut Ui, text: &str, enabled: bool) -> Respons ui.painter().rect_stroke(rect, CornerRadius::same(6), egui::Stroke::new(1.0, t.button_border), egui::StrokeKind::Inside); } ui.painter().galley(rect.center() - galley.size() / 2.0, galley, fg); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled && ui.is_enabled(), tl!(text))); resp } @@ -114,6 +128,7 @@ pub fn secondary_button(ui: &mut Ui, text: &str) -> Response { let (rect, resp) = ui.allocate_exact_size(vec2(w.max(72.0), 28.0), Sense::click()); ui.painter().rect_filled(rect, CornerRadius::same(6), if resp.hovered() { t.hover } else { t.button }); ui.painter().galley(rect.center() - galley.size() / 2.0, galley, t.text); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(text))); resp } @@ -657,6 +672,11 @@ pub fn proxy_chip(ui: &Ui, rect: Rect, paint: &Paint, mixed: bool, ring: Option< /// bar's Style picker): `draw` paints the `size`-square chip, which gets a frame, and with /// `chevron` a chevron follows it. pub fn chip_button(ui: &mut Ui, size: f32, chevron: bool, draw: impl FnOnce(&Ui, Rect)) -> Response { + chip_button_named(ui, size, chevron, "", draw) +} + +/// [`chip_button`] with the tool tip `tip`, which screen readers also call it (none when empty). +pub fn chip_button_named(ui: &mut Ui, size: f32, chevron: bool, tip: &str, draw: impl FnOnce(&Ui, Rect)) -> Response { let t = Tokens::get(ui.ctx()); let (r, resp) = ui.allocate_exact_size(vec2(if chevron { size + 16.0 } else { size }, size), Sense::click()); let chip = Rect::from_min_size(r.min, vec2(size, size)).shrink(2.0); @@ -665,7 +685,11 @@ pub fn chip_button(ui: &mut Ui, size: f32, chevron: bool, draw: impl FnOnce(&Ui, if chevron { icons::paint(ui, "chevron-down", Rect::from_center_size(pos2(chip.right() + 9.0, r.center().y), vec2(12.0, 12.0)), t.text_dim); } - resp + if tip.is_empty() { + return resp; + } + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(tip))); + resp.on_hover_text(tl!(tip)) } /// What the user did on a [`fill_stroke_proxy`]. @@ -732,6 +756,13 @@ pub fn fill_stroke_proxy(ui: &mut Ui, fill: &Paint, stroke: &Paint, mixed: (bool } else { None }; + let on = ui.is_enabled(); + fill_resp + .widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, on, fill_active, tl!("Fill (X), double-click for the Color Picker"))); + stroke_resp + .widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, on, !fill_active, tl!("Stroke (X), double-click for the Color Picker"))); + swap.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, on, tl!("Swap Fill and Stroke (Shift+X)"))); + def.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, on, tl!("Default Fill and Stroke (D)"))); ProxyClicks { fill: fill_resp.on_hover_text(tl!("Fill (X), double-click for the Color Picker")).clicked(), stroke: stroke_resp.on_hover_text(tl!("Stroke (X), double-click for the Color Picker")).clicked(), @@ -834,6 +865,12 @@ fn searchable_combo(ui: &mut Ui, id: egui::Id, current: &str, width: f32, lis ); let height = (galley.size().y.max(icon.y) + 2.0 * pad.y).max(ui.spacing().interact_size.y); let (rect, resp) = ui.allocate_exact_size(vec2(width, height), Sense::click()); + // Screen readers get what egui's combo box gives them: its role and the text it shows. + resp.widget_info(|| egui::WidgetInfo { + enabled: ui.is_enabled(), + current_text_value: Some(current.to_owned()), + ..egui::WidgetInfo::new(egui::WidgetType::ComboBox) + }); if ui.is_rect_visible(rect) { let v = if open { ui.visuals().widgets.open } else { *ui.style().interact(&resp) }; ui.painter().rect(rect.expand(v.expansion), v.corner_radius, v.weak_bg_fill, v.bg_stroke, StrokeKind::Inside); @@ -1027,6 +1064,7 @@ pub fn opacity_blend( let (r, resp) = ui.allocate_exact_size(vec2(18.0, 26.0), if enabled { Sense::click() } else { Sense::hover() }); ui.painter().rect_stroke(r, 2, Stroke::new(1.0, t.input_border), StrokeKind::Inside); icons::paint(ui, "chevron-right", r.shrink2(vec2(3.0, 7.0)), if enabled { t.icon } else { t.text_disabled }); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled && ui.is_enabled(), tl!("Opacity"))); egui::Popup::menu(&resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| { let mut o = opacity.unwrap_or(1.0) * 100.0; let r = slider(ui, egui::Slider::new(&mut o, 0.0..=100.0).show_value(false)); @@ -1057,9 +1095,22 @@ pub fn reference_point(ui: &mut Ui, current: usize) -> Option { let line = Stroke::new(1.0, t.text_dim); ui.painter().rect_stroke(Rect::from_two_pos(centre(0), centre(8)), 0.0, line, StrokeKind::Middle); let mut out = None; - for i in 0..9 { + // Screen readers name the squares left to right and top to bottom. + let names = [ + tl!("Top Left"), + tl!("Top"), + tl!("Top Right"), + tl!("Left"), + tl!("Center"), + tl!("Right"), + tl!("Bottom Left"), + tl!("Bottom"), + tl!("Bottom Right"), + ]; + for (i, name) in names.into_iter().enumerate() { let r = Rect::from_center_size(centre(i), Vec2::splat(sq)); let resp = ui.interact(r.expand(2.0), ui.id().with(("refpt", i)), Sense::click()); + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, ui.is_enabled(), i == current, name)); if resp.clicked() { out = Some(i); } @@ -1086,6 +1137,7 @@ pub fn angle_dial(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, angle let a = (angle as f32).to_radians(); ui.painter().line_segment([c, c + vec2(a.cos(), -a.sin()) * (r - 3.0)], Stroke::new(1.5, t.text)); ui.painter().circle_filled(c, 2.0, t.text); + resp.widget_info(|| egui::WidgetInfo::slider(ui.is_enabled(), angle, tl!("Angle"))); let (p, _) = pointer_phase(&resp)?; let v = p - c; if v.length() < 2.0 { @@ -1107,6 +1159,7 @@ pub fn toggle_icon(ui: &mut Ui, on_icon: &str, on: bool, size: f32, tip: &str) - } else if resp.hovered() { icons::paint(ui, on_icon, rect.shrink(size * 0.2), t.text_disabled); } + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), on, tl!(tip))); resp.on_hover_text(tl!(tip)).clicked() } @@ -1121,9 +1174,16 @@ pub fn icon_button_enabled(ui: &mut Ui, icon: &str, tip: &str, selected: bool, e let (rect, resp) = ui.allocate_exact_size(Vec2::splat(size), Sense::hover()); let pad = (size * 0.2).round(); icons::paint(ui, icon, rect.shrink(pad), t.text_disabled); + resp.widget_info(|| icon_button_info(false, selected, tl!(tip))); if tip.is_empty() { resp } else { resp.on_hover_text(tl!(tip)) } } +/// What screen readers get for an icon button: a button called `name` (already translated), +/// reported as pressed while `selected` and with no pressed state otherwise. +pub(crate) fn icon_button_info(enabled: bool, selected: bool, name: &str) -> egui::WidgetInfo { + egui::WidgetInfo { selected: selected.then_some(true), ..egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled, name) } +} + /// A slider whose rail shows, filled up to the handle: the theme's widget fill matches the panel /// and dialog background, which leaves only the handle visible (#1483). pub fn slider(ui: &mut Ui, slider: egui::Slider<'_>) -> Response { @@ -1157,6 +1217,7 @@ pub fn link_label(ui: &mut Ui, text: &str) -> Response { ui.painter().line_segment([pos2(x, y), pos2((x + 1.0).min(rect.right()), y)], Stroke::new(1.0, t.text_dim)); x += 2.5; } + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!(text))); resp } @@ -1202,9 +1263,20 @@ pub fn check_name(ui: &mut Ui, name: &str, value: bool) -> bool { check_row(ui, name, Some(value), true).0 } +/// A [`check`] box without text, for a row whose label is drawn beside it in another column: +/// screen readers call it `name` (already translated). Returns true when toggled. +pub fn check_box(ui: &mut Ui, name: &str, value: bool, enabled: bool) -> bool { + check_row_named(ui, "", name, Some(value), enabled).0 +} + /// [`check3`] with its `text` already translated, also returning the row's response (what a /// tooltip goes on: a widget around it never counts as hovered under the box). fn check_row(ui: &mut Ui, text: &str, value: Option, enabled: bool) -> (bool, Response) { + check_row_named(ui, text, text, value, enabled) +} + +/// [`check_row`] that screen readers call `name` instead of its `text`. +fn check_row_named(ui: &mut Ui, text: &str, name: &str, value: Option, enabled: bool) -> (bool, Response) { let t = Tokens::get(ui.ctx()); let (bx, resp, border) = choice_row(ui, text, enabled); let checked = value == Some(true); @@ -1222,18 +1294,34 @@ fn check_row(ui: &mut Ui, text: &str, value: Option, enabled: bool) -> (bo } Some(false) => {} } + resp.widget_info(|| match value { + Some(on) => egui::WidgetInfo::selected(egui::WidgetType::Checkbox, enabled && ui.is_enabled(), on, name), + None => egui::WidgetInfo::labeled(egui::WidgetType::Checkbox, enabled && ui.is_enabled(), name), + }); (enabled && resp.clicked(), resp) } /// Radio button in the style of [`check`], with a disabled state. Returns true when clicked. pub fn radio(ui: &mut Ui, label: &str, selected: bool, enabled: bool) -> bool { + radio_row(ui, tl!(label), tl!(label), selected, enabled) +} + +/// A [`radio`] button without text, for one followed by a field and the text that names it: +/// screen readers call it `name` (already translated). Returns true when clicked. +pub fn radio_button(ui: &mut Ui, name: &str, selected: bool, enabled: bool) -> bool { + radio_row(ui, "", name, selected, enabled) +} + +/// [`radio`] with its `text` already translated, which screen readers call `name`. +fn radio_row(ui: &mut Ui, text: &str, name: &str, selected: bool, enabled: bool) -> bool { let t = Tokens::get(ui.ctx()); - let (bx, resp, border) = choice_row(ui, tl!(label), enabled); + let (bx, resp, border) = choice_row(ui, text, enabled); let c = bx.center(); ui.painter().circle(c, 6.0, if selected && enabled { t.accent_strong } else { t.input }, Stroke::new(1.0, border)); if selected { ui.painter().circle_filled(c, 2.5, if enabled { Color32::WHITE } else { t.text_disabled }); } + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::RadioButton, enabled && ui.is_enabled(), selected, name)); enabled && resp.clicked() } @@ -1348,6 +1436,7 @@ fn spin_generic( ui.painter().line_segment([m + vec2(-3.0, -d), m + vec2(0.0, d)], Stroke::new(1.2, c)); ui.painter().line_segment([m + vec2(0.0, d), m + vec2(3.0, -d)], Stroke::new(1.2, c)); } + sresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled, tl!("Step up or down"))); // A blank field (values that differ) has nothing to step from. if sresp.clicked() && let Some(p) = sresp.interact_pointer_pos() @@ -1405,6 +1494,19 @@ pub fn color_slider( value: f32, width: f32, track: &dyn Fn(f32) -> Color32, +) -> (Option, Live) { + color_slider_named(ui, id, "", value, width, track) +} + +/// [`color_slider`] that screen readers get as a slider named `name` (already translated) with no +/// value (none when `name` is empty). +pub fn color_slider_named( + ui: &mut Ui, + id: impl std::hash::Hash + std::fmt::Debug, + name: &str, + value: f32, + width: f32, + track: &dyn Fn(f32) -> Color32, ) -> (Option, Live) { let t = Tokens::get(ui.ctx()); let (rect, _) = ui.allocate_exact_size(vec2(width, 22.0), Sense::hover()); @@ -1418,6 +1520,9 @@ pub fn color_slider( let thumb = vec![tip, pos2(x + 5.5, tip.y + 6.0), pos2(x + 5.5, tip.y + 10.0), pos2(x - 5.5, tip.y + 10.0), pos2(x - 5.5, tip.y + 6.0)]; let fill = if resp.dragged() || resp.hovered() { Color32::WHITE } else { t.icon }; ui.painter().add(egui::Shape::convex_polygon(thumb, fill, Stroke::new(1.0, t.border))); + if !name.is_empty() { + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Slider, ui.is_enabled(), name)); + } match pointer_phase(&resp) { Some((p, phase)) => (Some(((p.x - bar.left()) / bar.width()).clamp(0.0, 1.0)), phase), None => (None, Live::Idle), @@ -1506,6 +1611,19 @@ pub fn channel_slider( value: f32, size: Vec2, track: &dyn Fn(f32) -> Color32, +) -> (Option, Live) { + channel_slider_named(ui, id, "", value, size, track) +} + +/// [`channel_slider`] that screen readers get as a slider named `name` (already translated) with +/// no value, as for [`color_slider_named`] (none when `name` is empty). +pub fn channel_slider_named( + ui: &mut Ui, + id: impl std::hash::Hash + std::fmt::Debug, + name: &str, + value: f32, + size: Vec2, + track: &dyn Fn(f32) -> Color32, ) -> (Option, Live) { let t = Tokens::get(ui.ctx()); let (rect, _) = ui.allocate_exact_size(size, Sense::hover()); @@ -1519,6 +1637,9 @@ pub fn channel_slider( let tip = pos2(x + d * 5.0, y); ui.painter().add(egui::Shape::convex_polygon(vec![tip, pos2(x, y + 4.0 * d), pos2(x, y - 4.0 * d)], fill, Stroke::NONE)); } + if !name.is_empty() { + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Slider, ui.is_enabled(), name)); + } match pointer_phase(&resp) { Some((p, phase)) => (Some(((bar.bottom() - p.y) / bar.height()).clamp(0.0, 1.0)), phase), None => (None, Live::Idle), @@ -1881,13 +2002,15 @@ pub fn harmony_wheel(ui: &mut Ui, id: &str, size: f32, colors: &[Color], base: O let h = (-dv.y).atan2(dv.x).to_degrees().rem_euclid(360.0); out.moved = Some((i, [h, (dv.length() / r).clamp(0.0, 1.0), old.to_hsb()[2]])); } + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Slider, ui.is_enabled(), tl!("Color wheel"))); }); ui.add_space(6.0); ui.horizontal(|ui| { ui.add_space(((ui.available_width() - size - 76.0) / 2.0).max(0.0)); ui.add_sized([70.0, 22.0], egui::Label::new(egui::RichText::new(tl!("Brightness")).color(t.text_dim))); if let (Some(b), Some([h, s, _])) = (base, base_hsb) - && let (Some(nv), _) = color_slider(ui, (id, "brightness"), v, size, &|x| crate::panels::c32(&Color::from_hsb(h, s, x))) + && let (Some(nv), _) = + color_slider_named(ui, (id, "brightness"), tl!("Brightness"), v, size, &|x| crate::panels::c32(&Color::from_hsb(h, s, x))) { out.moved = Some((b, [h, s, nv])); } @@ -1916,6 +2039,7 @@ pub fn tab_bar(ui: &mut Ui, tabs: &[&str], current: usize) -> Option { t.accent, ); } + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::SelectableLabel, ui.is_enabled(), sel, tl!(tab))); if resp.clicked() { clicked = Some(i); } @@ -1942,6 +2066,7 @@ pub fn orientation_button(ui: &mut Ui, landscape: bool, selected: bool, tip: &st ui.painter().add(egui::Shape::convex_polygon(outline.clone(), fill, Stroke::new(1.2, color))); let corner = [page.right_top() - vec2(fold, 0.0), page.right_top() + vec2(-fold, fold), page.right_top() + vec2(0.0, fold)]; ui.painter().add(egui::Shape::line(corner.to_vec(), Stroke::new(1.2, if selected { t.panel } else { color }))); + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), selected, tl!(tip))); resp.on_hover_text(tl!(tip)).clicked() } @@ -1987,6 +2112,7 @@ pub fn layout_button(ui: &mut Ui, tiles: &[(u8, u8)], selected: bool, tip: &str) ui.painter().add(egui::Shape::line(path, Stroke::new(1.2, color))); ui.painter().add(egui::Shape::convex_polygon(head, color, Stroke::NONE)); } + resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, enabled, selected, tip)); resp.on_hover_text(tip).clicked() } @@ -2020,6 +2146,7 @@ fn chevron_cell(ui: &mut Ui, h: f32) -> Response { t.icon }; icons::paint(ui, "chevron-down", Rect::from_center_size(dr.center(), Vec2::splat(12.0)), c); + dresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!("Presets"))); dresp } @@ -2044,6 +2171,159 @@ pub fn text_presets(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, val out } +#[cfg(test)] +pub(crate) mod accessibility_tests { + use egui::accesskit::{Node, Role, Toggled}; + + use super::*; + + /// The screen-reader tree of the second of two frames of `add` in a 1280 × 900 window, in + /// English. + pub(crate) fn tree(mut add: impl FnMut(&mut Ui)) -> Vec { + crate::i18n::set_current(crate::i18n::Lang::EN); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + theme::install_fonts(&ctx); + theme::apply(&ctx, Default::default()); + let screen = Rect::from_min_size(Pos2::ZERO, vec2(1280.0, 900.0)); + let mut nodes = vec![]; + for time in [0.0, 0.1] { + let input = egui::RawInput { time: Some(time), screen_rect: Some(screen), ..Default::default() }; + let mut out = ctx.run_ui(input, &mut add); + out.textures_delta.clear(); + nodes = out.platform_output.accesskit_update.map(|u| u.nodes.into_iter().map(|(_, n)| n).collect()).unwrap_or_default(); + } + nodes + } + + /// The node of `tree` that screen readers name `label`. + pub(crate) fn named<'a>(tree: &'a [Node], label: &str) -> &'a Node { + tree.iter().find(|n| n.label() == Some(label)).unwrap_or_else(|| panic!("nothing is named {label:?}")) + } + + /// The role of the node named `label`, and whether it is pressed, checked or chosen. + pub(crate) fn role_and_state(tree: &[Node], label: &str) -> (Role, Option) { + let n = named(tree, label); + (n.role(), n.toggled()) + } + + #[test] + fn buttons_are_named_by_their_text_or_tip() { + let tree = tree(|ui| { + icon_button(ui, "x", "Remove", false, 24.0); + flat_button(ui, "Make", 64.0); + split_button_named(ui, "Image Trace", "Tracing Presets", 104.0); + primary_button(ui, "OK"); + primary_button_enabled(ui, "Create", false); + secondary_button(ui, "Cancel"); + link_label(ui, "Stroke:"); + icon_button_enabled(ui, "trash-2", "Delete Brush", false, false, 24.0); + fill_stroke_proxy(ui, &Paint::None, &Paint::None, (false, false), true, 36.0); + chip_button_named(ui, 22.0, true, "Graphic Style", |_, _| {}); + spin_field(ui, "weight", Some(1.0), Unit::Points, 80.0, 1.0, 0.0, &[1.0, 2.0]); + opacity_blend(ui, "opacity", Some(1.0), Some(BlendMode::Normal), true); + }); + for label in [ + "Remove", + "Make", + "Image Trace", + "Tracing Presets", + "OK", + "Create", + "Cancel", + "Stroke:", + "Delete Brush", + "Swap Fill and Stroke (Shift+X)", + "Graphic Style", + "Step up or down", + "Presets", + "Opacity", + ] { + assert_eq!(role_and_state(&tree, label), (Role::Button, None), "{label}"); + } + assert_eq!(role_and_state(&tree, "Default Fill and Stroke (D)"), (Role::Button, None)); + assert_eq!(role_and_state(&tree, "Fill (X), double-click for the Color Picker"), (Role::Button, Some(Toggled::True))); + assert_eq!(role_and_state(&tree, "Stroke (X), double-click for the Color Picker"), (Role::Button, Some(Toggled::False))); + assert!(named(&tree, "Create").is_disabled(), "a primary button that is not enabled"); + assert!(named(&tree, "Delete Brush").is_disabled(), "an icon button that is not enabled"); + assert!(!named(&tree, "OK").is_disabled()); + } + + #[test] + fn toggles_say_whether_they_are_on() { + let tree = tree(|ui| { + icon_button(ui, "link", "Constrain Proportions", true, 24.0); + toggle_icon(ui, "eye", true, 18.0, "Show all plates"); + toggle_icon(ui, "eye", false, 18.0, "Show/hide plate (Alt-click: only this plate)"); + orientation_button(ui, false, true, "Portrait"); + orientation_button(ui, true, false, "Landscape"); + layout_button(ui, &[(0, 0), (1, 0)], true, "Grid by Row"); + tab_bar(ui, &["Recent", "Saved"], 1); + }); + let on = |label| assert_eq!(role_and_state(&tree, label), (Role::Button, Some(Toggled::True)), "{label}"); + let off = |label| assert_eq!(role_and_state(&tree, label), (Role::Button, Some(Toggled::False)), "{label}"); + on("Constrain Proportions"); + on("Show all plates"); + off("Show/hide plate (Alt-click: only this plate)"); + on("Portrait"); + off("Landscape"); + on("Grid by Row"); + off("Recent"); + on("Saved"); + } + + #[test] + fn checkboxes_and_radio_buttons_are_named_and_checked() { + let tree = tree(|ui| { + check(ui, "Preview", true, true); + check(ui, "Snap to Pixel", false, false); + check3(ui, "Overprint Fill", None, true); + check_name(ui, "Artboard 1", true); + check_box(ui, "Cyan", false, true); + radio(ui, "Relative", true, true); + radio(ui, "Absolute", false, true); + radio_button(ui, "per slice", true, true); + reference_point(ui, 4); + }); + assert_eq!(role_and_state(&tree, "Preview"), (Role::CheckBox, Some(Toggled::True))); + assert_eq!(role_and_state(&tree, "Snap to Pixel"), (Role::CheckBox, Some(Toggled::False))); + assert!(named(&tree, "Snap to Pixel").is_disabled()); + assert_eq!(role_and_state(&tree, "Overprint Fill"), (Role::CheckBox, Some(Toggled::Mixed))); + assert_eq!(role_and_state(&tree, "Artboard 1"), (Role::CheckBox, Some(Toggled::True))); + assert_eq!(role_and_state(&tree, "Relative"), (Role::RadioButton, Some(Toggled::True))); + assert_eq!(role_and_state(&tree, "Absolute"), (Role::RadioButton, Some(Toggled::False))); + assert_eq!(role_and_state(&tree, "Cyan"), (Role::CheckBox, Some(Toggled::False))); + assert_eq!(role_and_state(&tree, "per slice"), (Role::RadioButton, Some(Toggled::True))); + for (i, label) in ["Top Left", "Top", "Top Right", "Left", "Center", "Right", "Bottom Left", "Bottom", "Bottom Right"].into_iter().enumerate() + { + let chosen = if i == 4 { Toggled::True } else { Toggled::False }; + assert_eq!(role_and_state(&tree, label), (Role::RadioButton, Some(chosen)), "{label}"); + } + } + + #[test] + fn sliders_dials_and_combo_boxes_are_named() { + let red = Color::from_hsb(0.0, 1.0, 1.0); + let tree = tree(|ui| { + color_slider_named(ui, "fidelity", "Fidelity:", 0.25, 120.0, &|_| Color32::GRAY); + angle_dial(ui, "angle", 45.0, 28.0); + channel_slider_named(ui, "channel", "H", 0.5, vec2(22.0, 80.0), &|_| Color32::GRAY); + harmony_wheel(ui, "wheel", 120.0, &[red], Some(0)); + combo(ui, "font", "Regular", 120.0, true, |_| None::); + }); + let slider = |label| { + let n = named(&tree, label); + (n.role(), n.numeric_value()) + }; + assert_eq!(slider("Fidelity:"), (Role::Slider, None)); + assert_eq!(slider("Angle"), (Role::Slider, Some(45.0))); + assert_eq!(slider("H"), (Role::Slider, None)); + assert_eq!(slider("Color wheel"), (Role::Slider, None)); + assert_eq!(slider("Brightness"), (Role::Slider, None)); + assert!(tree.iter().any(|n| n.role() == Role::ComboBox && n.value() == Some("Regular")), "the searchable combo box shows its text"); + } +} + #[cfg(test)] mod spinner_modifier_tests { use super::*; diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f8f68e48a..de7b0e9c4 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -431,6 +431,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - The contextual task bar's handle drags it anywhere on the canvas (kept whole on it as the window resizes); unpinned it keeps that offset as it follows the selection, and its More Options menu hides it, pins its position across selection changes (`window.taskBar.pin`), resets it under the selection (`window.taskBar.reset`) and shows the Properties panel. As in Illustrator the position isn't kept across launches. Not done: the bar's tool-specific variants (only the selection tools show it). - With one image selected, the contextual task bar offers Image Trace, which traces the image at once with the Default preset (the Image Trace panel is left as it was), and Crop Image; with an Image Trace object it offers Expand, which keeps only the traced paths. As in Illustrator ("Selecting images displays options like Image Trace, Retype, and Crop Image"; "You can also quickly expand traced artwork from the bar": [Contextual Task Bar announcement](https://community.adobe.com/announcements-651/contextual-task-bar-in-illustrator-2025-quick-actions-when-you-need-them-1546580)). Offset Path and Simplify, which don't apply to an image, aren't offered for one. Not done: Retype, which the app doesn't have. - Tool flyouts tear off: dragging or clicking the tear-off bar on a flyout's right side (or releasing the long press over it) floats its group as a strip of tool buttons, moved by the same bar and put back in the toolbar by the × at its top (`window.floatTools`). While a group floats, the presses that would open its flyout raise the strip instead. Floating strips are kept with the preferences and in user workspaces. Not done: turning a strip vertical, docking it. + - Screen readers get a name for the Tools panel's tool buttons, flyout rows, floating strips and tear-off bars, and for the icon buttons, text buttons, toggles, checkboxes, radio buttons, tabs, chips, sliders, angle dials and combo boxes that panels and dialogs draw with `crates/ui-egui/src/widgets.rs`: the text, label or tool tip each one shows, or a name of its own where it shows none (the angle dial, the spinner, the preset arrow, the color wheel and the Reference Point's squares). The tool buttons, toggles, checkboxes, radio buttons, tabs and the Reference Point's nine squares also report whether they are on, an icon button that is lit, such as the chosen Align To button, reports that it is pressed, and the angle dial reports its angle. The status bar's First, Previous, Next and Last Artboard buttons have tool tips. - Panels undock (#495): a dock tab, a panel icon or a popped-out panel's title dragged out of the dock floats that panel inside the window, and the strip right of the dock's tabs floats the whole tabbed group. A floating group moves by its title bar, a tab dragged out of it floats on its own, and dropped on another group's title bar or tabs it stacks with that group; dropped on the dock, which lights up under the pointer, its panels go back where they live (the tabbed group or the icon column), as its × and the panel menus' Float Panel / Dock Panel do (`window.panel.float`, `window.panel.dock`). The Tools panel floats by its title bar and docks again on the window's left edge. Floating groups stack into sets: dropped on the upper or lower half of another group (a line shows where), a group joins it right above or below it, and the set's groups stack top to bottom, each showing one panel, moving together by the top group's title bar, whose × docks them all; a group dragged by its tab strip leaves the set (`window.panel.float {below, above}`). A double-click on a tab collapses a floating group to its tabs (`collapsed`). Sets dropped on the left edge of the icon column or of another dock column dock as dock columns (a multi-column dock, `window.panel.dock {column}`), whose groups stack, tab and tear out as a floating set's do, and whose top strip floats the column again. Floating panels, their sets and the Tools panel's position are kept with the preferences and in user workspaces, and clamped into a smaller window. Not done: panels in separate OS windows, docking a panel into the default tabbed group it doesn't belong to, reordering tabs by dragging, collapsing a set or dock column to icons, dock columns right of the default dock, resizing floating panels and the groups of a set. - The dock's double arrow collapses Properties, Layers and Libraries to icons at the top of the icon column, popping out like the other icon panels, and expands them again (`window.collapseDock`, kept with the preferences and in saved workspaces). The icon column's own arrow (expanding the icon panels into a full column) is not done. - Czech render-site labels: desktop/web file filters, diagnostics, suggested preset names and contributor tooltips/plurals now use catalog lookups; gradients have Czech adjective agreement and printer marks have a separate context (PDF/Print and Summary). Search, Home, toolbar/panel tooltips and Preferences already look up their labels; new variable keys fall back to English in other languages. Evidence: `i18n::tests::czech_render_site_follow_up`, `dialogs::tests_modal::printer_mark_labels_are_distinct_from_language_names`; see [localization-parity.md](localization-parity.md).