mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 17:56:50 -06:00
Accessibility: buttons, toggles, checkboxes and sliders have names for screen readers
The controls that the shared widgets draw had no name in the accessibility tree: icon, flat, primary, secondary and split buttons, chips, toggles, checkboxes, radio buttons, tabs, sliders, the angle dial, the Reference Point's squares, the spinner and preset arrows and the searchable combo boxes. Screen readers announced each clickable one as an unnamed element of unknown role, and a disabled icon button was not in the tree. The Color Guide's library button, the Color Themes Save button and the status bar's artboard buttons also had no name. Each helper in widgets.rs except color_field now gives its response a role, a name and its state. The name is the text, label or translated tool tip the helper shows. The spinner, the preset arrow, the angle dial, the harmony wheel and the Reference Point's squares, which show none, are named "Step up or down", "Presets", "Angle", "Color wheel" and Top Left to Bottom Right. A toggle, checkbox, radio button, tab or Reference Point square reports whether it is on, an icon button reports that it is pressed while it is lit, the angle dial reports its angle, and a searchable combo box reports the text it shows. A checkbox or radio button drawn without text is named by the label beside it. The Tools panel's tool buttons, flyout rows and floating strips are named by their tools' tool tips, with the active tool pressed. The title bar's column toggle, the tear-off bars, a floating strip's close box and the Color, Gradient and None buttons are named by their tool tips. The status bar's First, Previous, Next and Last Artboard buttons now have tool tips, and the Fill and Stroke chips' tool tips are translated. The six new strings (the four artboard tips, "Color wheel" and "Step up or down") have rows in every catalog. split_button, chip_button, color_slider, channel_slider and the dialog form's slider_field and slider_rail keep their arguments. New _named variants beside them take the arrow's tool tip, the chip's tool tip or the slider's name, and every caller in the tree uses them. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -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<egui::Event>) -> Vec<(String, egui::Rect)> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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<f64>,
|
||||
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<f64>,
|
||||
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<f64>, 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<f64>,
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -688,7 +688,7 @@ fn output(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result<Value, String>) {
|
||||
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() };
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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())]);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 1902 and column 19.
|
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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ó
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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°刻みに調整。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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°씩 스냅됩니다.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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ć
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -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°.
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 721 and column 40.
|
@@ -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.
|
||||
|
||||
|
@@ -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° 为步长调整。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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° 為間距調整。
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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() {
|
||||
|
||||
@@ -761,7 +761,7 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, 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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
);
|
||||
}
|
||||
crate::floating::drag_tools(app, ui.ctx(), &hresp.union(gresp.clone()), bounds, floating);
|
||||
hresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), tl!("Toggle single/double column")));
|
||||
if hresp.on_hover_text(tl!("Toggle single/double column")).clicked()
|
||||
// It only fails on bad params, which this never sends; show it all the same.
|
||||
&& let Err(e) = app.run("window.toolbarColumns", json!({ "double": cols == 1 }))
|
||||
@@ -168,6 +169,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
app.ui.status = e;
|
||||
}
|
||||
let grip_tip = if floating.is_some() { tl!("Drag to the window's left edge to dock") } else { tl!("Drag to float the toolbar") };
|
||||
gresp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled(), grip_tip));
|
||||
gresp.on_hover_cursor(egui::CursorIcon::Grab).on_hover_text(grip_tip);
|
||||
// The tools and the controls under them scroll in a window too short for them.
|
||||
widgets::strip_scroll(ui, "toolbar", |ui| {
|
||||
@@ -260,6 +262,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, cols: usize, floating: Option<egu
|
||||
} else if resp.clicked() {
|
||||
app.select_tool(shown.id);
|
||||
}
|
||||
resp.widget_info(|| egui::WidgetInfo::selected(egui::WidgetType::Button, ui.is_enabled(), is_active, tip(shown)));
|
||||
resp.on_hover_text(tip(shown));
|
||||
}
|
||||
});
|
||||
@@ -474,6 +477,7 @@ fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens, cols: usiz
|
||||
"paint.lastGradient" => 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<Event>) -> Vec<egui::Rect> {
|
||||
@@ -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<String> = ["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<String> = ["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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<R>(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<usize> {
|
||||
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<bool>, 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<bool>, 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<bool>, 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<f32>, 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<f32>, 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<f32>, 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<f32>, 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<usize> {
|
||||
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<Node> {
|
||||
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<Toggled>) {
|
||||
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::<usize>);
|
||||
});
|
||||
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::*;
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user