mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Dialogs: sliders step their field's number with the arrow keys
A dialog's slider could take the keyboard focus from Tab, but it responded only to the pointer, so an arrow key moved the focus to the nearest widget in the arrow's direction and a keyboard user could not move the slider. Screen readers could not step it either. These dialog sliders, each beside a number field, now respond to the keyboard. A click or Tab gives a slider the focus, which an outline shows. Then the Right and Up arrows step the field's number up to the next whole number and the Left and Down arrows step it down to the next whole number. Shift with an arrow, Page Up and Page Down step it by ten, and Home and End set the slider's first and last value. No step goes past either end, and a number past the slider's range steps from its nearer end. Screen readers' increment and decrement actions step the slider as the arrows do. A step sets the field's number in the field's own unit, so a Move slider in a millimeter document steps one millimeter. When the field holds a number past the slider's range, Home, End or a drag to that end of a form slider now sets the end's number; before, such a drag left the field's number as it was. The form sliders get this through the new color_slider_stepped, next to color_slider_valued, and so do the Pencil, Paintbrush and Smooth Fidelity, the Perspective Grid's Opacity, the 3D dialogs' sliders and the flattener's balance, which the Flattener Preview panel also shows. The Color panel, Swatch Options, the Color Picker's channel slider, the Navigator and the Character panel keep their sliders as they were. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -192,7 +192,7 @@ pub(crate) fn options_editor(ui: &mut egui::Ui, id: &str, o: &mut FlattenOptions
|
||||
dim(ui, tl!("Rasters"));
|
||||
let rail = (ui.available_width() - FIELD - 56.0).clamp(80.0, 200.0);
|
||||
let shown = widgets::SliderValue::field(o.balance, 0.0..=100.0, "", 0);
|
||||
if let (Some(x), _) = widgets::color_slider_valued(
|
||||
let (x, _, keyed) = widgets::color_slider_stepped(
|
||||
ui,
|
||||
(id, "balance-slider"),
|
||||
tl!("Raster/Vector Balance:"),
|
||||
@@ -200,8 +200,9 @@ pub(crate) fn options_editor(ui: &mut egui::Ui, id: &str, o: &mut FlattenOptions
|
||||
(o.balance / 100.0) as f32,
|
||||
rail,
|
||||
&|_| t.input_border,
|
||||
) {
|
||||
o.balance = (x as f64 * 100.0).round();
|
||||
);
|
||||
if let Some(b) = x.map(|x| (x as f64 * 100.0).round()).or(keyed) {
|
||||
o.balance = b;
|
||||
}
|
||||
dim(ui, tl!("Vectors"));
|
||||
if let Some(v) = widgets::plain_field(ui, (id, "balance"), o.balance, "", 0, FIELD - 12.0) {
|
||||
|
||||
@@ -648,8 +648,10 @@ pub(super) fn slider_field_named(
|
||||
}
|
||||
|
||||
/// The rail of [`slider_field_named`] 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); screen
|
||||
/// readers call it `name` (already translated), with the number `d.fields[key]` as its value.
|
||||
/// whole number in `range`, and its keys step the number while it has the keyboard focus
|
||||
/// ([`crate::widgets::color_slider_stepped`]); a value past the range shows at its end until the
|
||||
/// rail moves. Screen readers call it `name` (already translated), with the number
|
||||
/// `d.fields[key]` as its value.
|
||||
pub(super) fn slider_rail_named(
|
||||
ui: &mut egui::Ui,
|
||||
d: &mut Dialog,
|
||||
@@ -663,7 +665,9 @@ pub(super) fn slider_rail_named(
|
||||
}
|
||||
|
||||
/// [`slider_rail_named`] that screen readers get `shown` from: the number and text of the field
|
||||
/// the row draws beside it (no value for `None`, as for a blank field).
|
||||
/// the row draws beside it (no value for `None`, as for a blank field). Its keys step `shown`'s
|
||||
/// number, which sets `d.fields[key]` to the number at the same place on `range` (`shown`'s range
|
||||
/// and `range` are the same span in two units, such as millimeters and points).
|
||||
pub(super) fn slider_rail_valued(
|
||||
ui: &mut egui::Ui,
|
||||
d: &mut Dialog,
|
||||
@@ -674,10 +678,14 @@ pub(super) fn slider_rail_valued(
|
||||
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_valued(ui, ("dlg-slider", key), name, shown, ((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) {
|
||||
let held = d.f64(key, 0.0);
|
||||
let v = held.clamp(min, max);
|
||||
let units = shown.as_ref().map(|s| s.range.clone());
|
||||
let (dragged, _, keyed) =
|
||||
crate::widgets::color_slider_stepped(ui, ("dlg-slider", key), name, shown, ((v - min) / (max - min)) as f32, SLIDER_WIDTH, track);
|
||||
let keyed =
|
||||
keyed.zip(units).map(|(k, u)| if u == range { k } else { (min + (k - u.start()) / (u.end() - u.start()) * (max - min)).clamp(min, max) });
|
||||
if let Some(n) = dragged.map(|x| (min + x as f64 * (max - min)).round()).or(keyed).filter(|n| *n != held) {
|
||||
d.fields.insert(key.into(), json!(n));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +72,9 @@ fn fidelity(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
widgets::label_row(ui, tl!("Fidelity:"), label_w, |ui| {
|
||||
let at = ((v.clamp(min, max) - min) / (max - min)) as f32;
|
||||
let shown = widgets::SliderValue::field(v, min..=max, "pt", 1);
|
||||
if let (Some(x), _) =
|
||||
widgets::color_slider_valued(ui, "freehand-fidelity", tl!("Fidelity:"), Some(shown), at, form::SLIDER_WIDTH, &|_| t.input_border)
|
||||
{
|
||||
new = Some(((min + x as f64 * (max - min)) * 10.0).round() / 10.0);
|
||||
}
|
||||
let (x, _, keyed) =
|
||||
widgets::color_slider_stepped(ui, "freehand-fidelity", tl!("Fidelity:"), Some(shown), at, form::SLIDER_WIDTH, &|_| t.input_border);
|
||||
new = x.map(|x| ((min + x as f64 * (max - min)) * 10.0).round() / 10.0).or(keyed);
|
||||
if let Some(x) = widgets::plain_field(ui, "freehand-fidelity-field", v, "pt", 1, 64.0) {
|
||||
new = Some(x.clamp(0.05, 100.0));
|
||||
}
|
||||
|
||||
@@ -333,9 +333,9 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
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 shown = widgets::SliderValue::field(v, 0.0..=100.0, "%", 0);
|
||||
let mut new = widgets::color_slider_valued(ui, "persp-opacity", tl!("Opacity:"), Some(shown), (v / 100.0) as f32, form::SLIDER_WIDTH, &track)
|
||||
.0
|
||||
.map(|x| (f64::from(x) * 100.0).round());
|
||||
let (x, _, keyed) =
|
||||
widgets::color_slider_stepped(ui, "persp-opacity", tl!("Opacity:"), Some(shown), (v / 100.0) as f32, form::SLIDER_WIDTH, &track);
|
||||
let mut new = x.map(|x| (f64::from(x) * 100.0).round()).or(keyed);
|
||||
if let Some(x) = widgets::plain_field(ui, "persp-opacity-field", v, "%", 0, 52.0) {
|
||||
new = Some(x.round().clamp(0.0, 100.0));
|
||||
}
|
||||
|
||||
@@ -515,7 +515,7 @@ fn number(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, range: (f64,
|
||||
let places = ((f64::from(ui.input(|i| i.aim_radius())) / speed).log10().ceil().clamp(0.0, 15.0) as usize).min(decimals);
|
||||
let number = ui.style().number_formatter.format(value, places..=decimals);
|
||||
let shown = widgets::SliderValue::text(number.parse().unwrap_or(value), rail_min..=rail_max, format!("{number}{suffix}"));
|
||||
let slider = widgets::color_slider_valued(
|
||||
let slider = widgets::color_slider_stepped(
|
||||
ui,
|
||||
("revolve-slider", key),
|
||||
name,
|
||||
@@ -524,8 +524,8 @@ fn number(ui: &mut egui::Ui, d: &mut Dialog, key: &str, name: &str, range: (f64,
|
||||
rail,
|
||||
&|_| t.input_border,
|
||||
);
|
||||
if let (Some(n), _) = slider {
|
||||
next = Some(rail_min + f64::from(n) * (rail_max - rail_min));
|
||||
if let Some(n) = slider.0.map(|n| rail_min + f64::from(n) * (rail_max - rail_min)).or(slider.2) {
|
||||
next = Some(n);
|
||||
}
|
||||
if reset_dot(ui, key, (to(value) - default).abs() > 1e-6, &format!("{}{}", from(default), suffix)) {
|
||||
next = Some(from(default));
|
||||
|
||||
@@ -1512,6 +1512,170 @@ fn free_distort_reset_puts_the_corners_back() {
|
||||
assert_eq!((effects.len(), effects[0].params["corners"].clone()), (1, rest));
|
||||
}
|
||||
|
||||
/// The slider that screen readers name `label` in `out`'s tree: its node id, number and text.
|
||||
fn slider_node(out: &egui::FullOutput, label: &str) -> (egui::accesskit::NodeId, Option<f64>, Option<String>) {
|
||||
let nodes = &out.platform_output.accesskit_update.as_ref().expect("an accessibility tree").nodes;
|
||||
let (id, node) = nodes
|
||||
.iter()
|
||||
.find(|(_, n)| n.role() == egui::accesskit::Role::Slider && n.label() == Some(label))
|
||||
.unwrap_or_else(|| panic!("no slider named {label:?}"));
|
||||
(*id, node.numeric_value(), node.value().map(str::to_string))
|
||||
}
|
||||
|
||||
/// A form slider that a click gave the keyboard focus (Vector Halftone's Frequency, 1 to 150 lpi)
|
||||
/// steps the number of the field beside it with the arrow keys: one with → and ↑, back with ← and
|
||||
/// ↓, ten with Shift, and ten with Page Up and Page Down. Home and End take it to the ends of its
|
||||
/// range, a step stops there, and screen readers read each new number.
|
||||
#[test]
|
||||
fn a_focused_dialog_slider_steps_with_the_arrow_keys() {
|
||||
use egui::Key::{ArrowDown, ArrowLeft, ArrowRight, ArrowUp, End, Home, PageDown, PageUp};
|
||||
use egui::Modifiers as M;
|
||||
let mut app = app();
|
||||
open(&mut app, halftone::KIND, halftone::fields());
|
||||
let mut f = Frames::new();
|
||||
let at = f.control(&mut app, "Frequency", egui::accesskit::Role::Slider);
|
||||
f.press(&mut app, at, true);
|
||||
f.press(&mut app, at, false);
|
||||
let frequency = |app: &VectorcraftApp| app.ui.dialog.as_ref().expect("Vector Halftone is open").f64("frequency", 0.0);
|
||||
let start = frequency(&app);
|
||||
assert!((2.0..=139.0).contains(&start), "the click set {start}");
|
||||
let mut seen = vec![];
|
||||
for (key, mods, want) in [
|
||||
(ArrowRight, M::NONE, start + 1.0),
|
||||
(ArrowUp, M::NONE, start + 2.0),
|
||||
(ArrowLeft, M::NONE, start + 1.0),
|
||||
(ArrowDown, M::NONE, start),
|
||||
(ArrowRight, M::SHIFT, start + 10.0),
|
||||
(ArrowLeft, M::SHIFT, start),
|
||||
(Home, M::NONE, 1.0),
|
||||
(ArrowLeft, M::NONE, 1.0),
|
||||
(PageUp, M::NONE, 11.0),
|
||||
(End, M::NONE, 150.0),
|
||||
(ArrowUp, M::SHIFT, 150.0),
|
||||
(PageDown, M::NONE, 140.0),
|
||||
(PageUp, M::NONE, 150.0),
|
||||
] {
|
||||
f.key(&mut app, key, mods);
|
||||
seen.push((key, mods.shift, frequency(&app)));
|
||||
assert_eq!(frequency(&app), want, "{seen:?}");
|
||||
}
|
||||
f.key(&mut app, ArrowLeft, M::NONE);
|
||||
let out = f.run(&mut app, vec![]);
|
||||
assert_eq!((frequency(&app), slider_node(&out, "Frequency").1), (149.0, Some(149.0)));
|
||||
assert_eq!(slider_node(&out, "Frequency").2.as_deref(), Some("149 lpi"));
|
||||
}
|
||||
|
||||
/// Tab leaves a focused dialog slider for its field and Shift+Tab comes back to it, whose arrow
|
||||
/// keys then step the number again: the Perspective Grid's Opacity. OK gives the grid the new
|
||||
/// opacity.
|
||||
#[test]
|
||||
fn tab_and_shift_tab_reach_a_dialog_slider_for_the_arrow_keys() {
|
||||
use egui::Key::{ArrowRight, Tab};
|
||||
use egui::Modifiers as M;
|
||||
let mut app = app();
|
||||
app.run("ui.perspectiveGridDialog", json!({})).unwrap();
|
||||
let opacity = |app: &VectorcraftApp| app.ui.dialog.as_ref().expect("Define Grid is open").f64("opacity", 0.0);
|
||||
let mut f = Frames::new();
|
||||
let at = f.control(&mut app, "Opacity:", egui::accesskit::Role::Slider);
|
||||
f.press(&mut app, at, true);
|
||||
f.press(&mut app, at, false);
|
||||
let start = opacity(&app);
|
||||
f.key(&mut app, Tab, M::NONE);
|
||||
f.key(&mut app, ArrowRight, M::NONE);
|
||||
assert_eq!(opacity(&app), start, "the field has the focus, and → moves its caret");
|
||||
f.key(&mut app, Tab, M::SHIFT);
|
||||
f.key(&mut app, ArrowRight, M::NONE);
|
||||
f.key(&mut app, ArrowRight, M::SHIFT);
|
||||
assert_eq!(opacity(&app), start + 11.0);
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(app.run("perspective.grid.get", json!({})).unwrap()["define"]["opacity"].as_f64(), Some(start + 11.0));
|
||||
}
|
||||
|
||||
/// Screen readers can step a dialog slider: the Pencil's Fidelity, a field with tenths, goes to
|
||||
/// the next whole number on a decrement or an increment, as the slider's ← and → do.
|
||||
#[test]
|
||||
fn screen_readers_step_a_dialog_slider() {
|
||||
use egui::accesskit::{Action, ActionRequest, TreeId};
|
||||
let mut app = app();
|
||||
app.run("tool.options", json!({"tool": "pencil"})).unwrap();
|
||||
app.ui.dialog.as_mut().expect("Pencil Tool Options is open").fields.insert("fidelity".into(), json!(2.5));
|
||||
let fidelity = |app: &VectorcraftApp| app.ui.dialog.as_ref().expect("Pencil Tool Options is open").f64("fidelity", 0.0);
|
||||
let mut f = Frames::new();
|
||||
f.run(&mut app, vec![]);
|
||||
let out = f.run(&mut app, vec![]);
|
||||
let (node, ..) = slider_node(&out, "Fidelity:");
|
||||
let request = |action| egui::Event::AccessKitActionRequest(ActionRequest { action, target_node: node, target_tree: TreeId::ROOT, data: None });
|
||||
f.run(&mut app, vec![request(Action::Decrement)]);
|
||||
assert_eq!(fidelity(&app), 2.0, "from 2.5");
|
||||
f.run(&mut app, vec![request(Action::Increment)]);
|
||||
f.run(&mut app, vec![request(Action::Increment)]);
|
||||
assert_eq!(fidelity(&app), 4.0);
|
||||
}
|
||||
|
||||
/// Transform Each's sliders step the numbers of their fields: a Scale past the rail's 200% steps
|
||||
/// down from 200% and End sets 200%, → at the end leaves it as it is, and a Move slider steps one
|
||||
/// millimeter in a millimeter document.
|
||||
#[test]
|
||||
fn transform_each_sliders_step_the_numbers_their_fields_show() {
|
||||
use egui::Key::{ArrowLeft, ArrowRight, End, Tab};
|
||||
use egui::Modifiers as M;
|
||||
let mut app = app();
|
||||
app.run("document.setUnits", json!({"units": "millimeters"})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
crate::menus::invoke(&mut app, "object.transformEach", json!({}));
|
||||
let set =
|
||||
|app: &mut VectorcraftApp, k: &str, v: serde_json::Value| app.ui.dialog.as_mut().expect("Transform Each is open").fields.insert(k.into(), v);
|
||||
let get = |app: &VectorcraftApp, k: &str| app.ui.dialog.as_ref().expect("Transform Each is open").f64(k, f64::NAN);
|
||||
let mut f = Frames::new();
|
||||
let at = f.control(&mut app, "Horizontal:", egui::accesskit::Role::Slider);
|
||||
f.press(&mut app, at, true);
|
||||
f.press(&mut app, at, false);
|
||||
set(&mut app, "scaleH", json!(300));
|
||||
f.key(&mut app, ArrowRight, M::NONE);
|
||||
assert_eq!(get(&app, "scaleH"), 300.0, "→ at the rail's end");
|
||||
f.key(&mut app, End, M::NONE);
|
||||
assert_eq!(get(&app, "scaleH"), 200.0);
|
||||
set(&mut app, "scaleH", json!(300));
|
||||
f.key(&mut app, ArrowLeft, M::NONE);
|
||||
assert_eq!(get(&app, "scaleH"), 199.0);
|
||||
// Tab goes through the Scale fields and sliders to the Move slider.
|
||||
set(&mut app, "moveH", json!(0));
|
||||
for _ in 0..4 {
|
||||
f.key(&mut app, Tab, M::NONE);
|
||||
}
|
||||
f.key(&mut app, ArrowRight, M::NONE);
|
||||
assert!((get(&app, "moveH") - 72.0 / 25.4).abs() < 1e-9, "1 mm: {} pt", get(&app, "moveH"));
|
||||
}
|
||||
|
||||
/// The sliders the flattener options and the 3D dialogs draw take the arrow keys as well: Flatten
|
||||
/// Transparency's Raster/Vector Balance and 3D Revolve's angle each step by ten with Shift, and
|
||||
/// down to the next whole number with ↓.
|
||||
#[test]
|
||||
fn the_flattener_and_3d_sliders_step_with_the_arrow_keys() {
|
||||
use egui::Modifiers as M;
|
||||
let mut app = app();
|
||||
app.run("path.create", json!({"d": "M150 40 L180 60 L180 140 L150 160"})).unwrap();
|
||||
for (open, label, key) in [("flatten", "Raster/Vector Balance:", "balance"), ("revolve", "Revolve angle", "angle")] {
|
||||
if open == "flatten" {
|
||||
app.run("ui.flattenTransparencyDialog", json!({})).unwrap();
|
||||
} else {
|
||||
app.run("effect.dialog", json!({"effect": vectorcraft_effects::REVOLVE, "item": null})).unwrap();
|
||||
}
|
||||
let value = |app: &VectorcraftApp| app.ui.dialog.as_ref().expect("the dialog is open").f64(key, -1.0);
|
||||
let mut f = Frames::new();
|
||||
let at = f.control(&mut app, label, egui::accesskit::Role::Slider);
|
||||
f.press(&mut app, at, true);
|
||||
f.press(&mut app, at, false);
|
||||
let start = value(&app);
|
||||
f.key(&mut app, egui::Key::ArrowRight, M::SHIFT);
|
||||
assert_eq!(value(&app), start + 10.0, "{label}");
|
||||
// A plain step goes to the next whole number below.
|
||||
f.key(&mut app, egui::Key::ArrowDown, M::NONE);
|
||||
assert_eq!(value(&app), (start + 10.0).ceil() - 1.0, "{label} from {start}");
|
||||
app.ui.dialog = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn polar_grid_shared_canvas_click_defaults_units_counts_and_history() {
|
||||
use vectorcraft_doc::NodeId;
|
||||
|
||||
@@ -1711,10 +1711,120 @@ pub fn color_slider_valued(
|
||||
width: f32,
|
||||
track: &dyn Fn(f32) -> Color32,
|
||||
) -> (Option<f32>, Live) {
|
||||
let id = ui.id().with(id);
|
||||
let (_, picked, phase) = color_rail(ui, id, name, shown, value, width, track);
|
||||
(picked, phase)
|
||||
}
|
||||
|
||||
/// [`color_slider_valued`] for a dialog's rail beside a number field, which also responds to the
|
||||
/// keyboard. A click or a drag gives the rail the keyboard focus, which an outline around it shows.
|
||||
/// While it has the focus, → and ↑ raise the number `shown` holds to the next whole number and ←
|
||||
/// and ↓ lower it to the next whole number, Shift with an arrow, Page Up and Page Down add or take
|
||||
/// ten, and Home and End set the start and the end of `shown`'s range. Screen readers' increment
|
||||
/// and decrement actions step the number as → and ← do. A number past the range steps from the
|
||||
/// range's nearer end, and no step goes past either end. With `shown` `None` (a blank field), or
|
||||
/// in a disabled `ui`, the keys and actions leave the rail as it is. Returns the new value from 0
|
||||
/// to 1 that a click or a drag gives, its phase, and the number in `shown`'s units that a key or
|
||||
/// an action gives.
|
||||
pub fn color_slider_stepped(
|
||||
ui: &mut Ui,
|
||||
id: impl std::hash::Hash + std::fmt::Debug,
|
||||
name: &str,
|
||||
shown: Option<SliderValue>,
|
||||
value: f32,
|
||||
width: f32,
|
||||
track: &dyn Fn(f32) -> Color32,
|
||||
) -> (Option<f32>, Live, Option<f64>) {
|
||||
let id = ui.id().with(id);
|
||||
let (resp, picked, phase) = color_rail(ui, id, name, shown.clone(), value, width, track);
|
||||
if resp.drag_started() || resp.clicked() {
|
||||
resp.request_focus();
|
||||
}
|
||||
let focused = ui.memory(|m| m.has_focus(id));
|
||||
if focused {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.painter().rect_stroke(resp.rect, 2.0, Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
}
|
||||
let stepped = shown.filter(|_| ui.is_enabled()).and_then(|s| {
|
||||
ui.ctx().accesskit_node_builder(id, |node| {
|
||||
node.set_numeric_value_step(1.0);
|
||||
node.add_action(egui::accesskit::Action::Increment);
|
||||
node.add_action(egui::accesskit::Action::Decrement);
|
||||
});
|
||||
rail_step(ui, id, focused, &s)
|
||||
});
|
||||
(picked, phase, stepped.filter(|_| picked.is_none()))
|
||||
}
|
||||
|
||||
/// The number of slider `id` (`s`) after this frame's keys while it is `focused`, and after screen
|
||||
/// readers' actions on it ([`color_slider_stepped`]); `None` when they leave it as it was.
|
||||
fn rail_step(ui: &Ui, id: egui::Id, focused: bool, s: &SliderValue) -> Option<f64> {
|
||||
use egui::accesskit::Action;
|
||||
use egui::{Event, Key};
|
||||
let (lo, hi) = (*s.range.start(), *s.range.end());
|
||||
if !lo.is_finite() || !hi.is_finite() || hi <= lo {
|
||||
return None;
|
||||
}
|
||||
let from = s.value.clamp(lo, hi);
|
||||
let mut v = from;
|
||||
// Home or End: an end of the range, which also replaces a number past it.
|
||||
let mut end = false;
|
||||
if focused {
|
||||
// The arrows step the rail and do not move the focus. egui keeps the filter only when the
|
||||
// rail also had the focus in the frame before, and applies it from the next frame; until
|
||||
// then, an arrow the rail takes cancels the focus move that egui set at the start of the frame.
|
||||
let filter = egui::EventFilter { horizontal_arrows: true, vertical_arrows: true, ..Default::default() };
|
||||
ui.memory_mut(|m| m.set_focus_lock_filter(id, filter));
|
||||
let mut arrows = false;
|
||||
ui.input_mut(|i| {
|
||||
i.events.retain(|e| {
|
||||
let Event::Key { key, pressed: true, modifiers, .. } = e else { return true };
|
||||
let up = matches!(key, Key::ArrowRight | Key::ArrowUp | Key::PageUp);
|
||||
let n = if up { 1.0 } else { -1.0 };
|
||||
match key {
|
||||
Key::Home | Key::End if modifiers.is_none() => {
|
||||
v = if *key == Key::Home { lo } else { hi };
|
||||
end = true;
|
||||
}
|
||||
Key::PageUp | Key::PageDown if modifiers.is_none() => v = (v + 10.0 * n).clamp(lo, hi),
|
||||
Key::ArrowRight | Key::ArrowUp | Key::ArrowLeft | Key::ArrowDown if modifiers.shift_only() => {
|
||||
v = (v + 10.0 * n).clamp(lo, hi);
|
||||
}
|
||||
Key::ArrowRight | Key::ArrowUp | Key::ArrowLeft | Key::ArrowDown if modifiers.is_none() => {
|
||||
v = step_to_multiple(v, 1.0, n).clamp(lo, hi);
|
||||
arrows = true;
|
||||
}
|
||||
_ => return true,
|
||||
}
|
||||
false
|
||||
});
|
||||
});
|
||||
if arrows {
|
||||
ui.memory_mut(|m| m.move_focus(egui::FocusDirection::None));
|
||||
}
|
||||
}
|
||||
ui.input(|i| {
|
||||
let n = i.num_accesskit_action_requests(id, Action::Increment) as f64 - i.num_accesskit_action_requests(id, Action::Decrement) as f64;
|
||||
v = step_to_multiple(v, 1.0, n).clamp(lo, hi);
|
||||
});
|
||||
(v != from || (end && v != s.value)).then_some(round_to(v, STEP_DECIMALS).clamp(lo, hi))
|
||||
}
|
||||
|
||||
/// The rail of [`color_slider_valued`] with the id `id`: its response, the new value from 0 to 1
|
||||
/// that a click or drag gives and the phase.
|
||||
fn color_rail(
|
||||
ui: &mut Ui,
|
||||
id: egui::Id,
|
||||
name: &str,
|
||||
shown: Option<SliderValue>,
|
||||
value: f32,
|
||||
width: f32,
|
||||
track: &dyn Fn(f32) -> Color32,
|
||||
) -> (Response, Option<f32>, Live) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (rect, _) = ui.allocate_exact_size(vec2(width, 22.0), Sense::hover());
|
||||
let bar = Rect::from_min_size(pos2(rect.left() + 4.0, rect.top() + 4.0), vec2(width - 8.0, 7.0));
|
||||
let resp = ui.interact(rect, ui.id().with(id), Sense::click_and_drag());
|
||||
let resp = ui.interact(rect, id, Sense::click_and_drag());
|
||||
gradient_strip(ui, bar, (bar.width() / 2.0).ceil().max(2.0) as usize, false, track);
|
||||
ui.painter().rect_stroke(bar, 0.0, Stroke::new(1.0, t.border), StrokeKind::Outside);
|
||||
let v = value.clamp(0.0, 1.0);
|
||||
@@ -1724,10 +1834,11 @@ pub fn color_slider_valued(
|
||||
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)));
|
||||
slider_info(ui, &resp, name, shown);
|
||||
match pointer_phase(&resp) {
|
||||
let (picked, phase) = match pointer_phase(&resp) {
|
||||
Some((p, phase)) => (Some(((p.x - bar.left()) / bar.width()).clamp(0.0, 1.0)), phase),
|
||||
None => (None, Live::Idle),
|
||||
}
|
||||
};
|
||||
(resp, picked, phase)
|
||||
}
|
||||
|
||||
/// The pointer of a click or drag on `resp` and its phase (dragging, or released on click/drop).
|
||||
|
||||
@@ -479,6 +479,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- With anchor points selected with Direct Selection, the contextual task bar offers Remove Anchor Points, Connect (`path.join`), Cut Path, Corner and Smooth (`path.convertAnchors` with `to` set to `corner` or `smooth`), the commands of the Control bar's anchor buttons. As in Illustrator ("Remove selected anchor points, Connect selected end points, Cut path at selected anchor points, Convert selected anchor points to corner, and Convert selected anchor points to smooth": [Contextual Task Bar overview](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/contextual-task-bar-overview.html)). Not done: Simplify, which the bar offers only while no anchors are selected, and the bar's Smooth item that adjusts a path's curvature, a command the app doesn't have (Object › Path › Smooth).
|
||||
- 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.
|
||||
- These dialog sliders, each beside a number field, respond to the keyboard: those of the effect, tool option, brush and transform dialogs, the Perspective Grid, the 3D dialogs, and the flattener options, which the Flattener Preview panel also shows. A click, or Tab, gives a slider the keyboard focus, and an outline shows it. Then → and ↑ step the field's number up to the next whole number and ← and ↓ step it down to the next whole number, Shift with an arrow and Page Up and Page Down step it by ten, and Home and End set the slider's first and last value; no step goes past either end. Screen readers' increment and decrement actions step the slider as → and ← do. The Color panel's, Swatch Options', the Navigator's and the Character panel's sliders, and the Color Picker's channel slider, respond only to the pointer. The keys follow the WAI-ARIA slider pattern (<https://www.w3.org/WAI/ARIA/apg/patterns/slider/>); the reference app's help does not describe keys on a dialog's slider, so the step sizes are VectorCraft's own.
|
||||
- Screen readers get the value of the color and channel sliders that panels and dialogs draw (the Color panel, the Color Picker, Swatch Options, the Navigator's Zoom, the Character panel's variable font axes, the Perspective Grid and flattener options, and the sliders of the effect, tool option and transform dialogs): the number and text of the field beside each one, such as 128 for G or 50% for an opacity, with the slider's range, which extends to include a number past the slider's end. The harmony wheel's Brightness slider, which has no field, reports its brightness in percent.
|
||||
- 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.
|
||||
|
||||
Reference in New Issue
Block a user