Effects angle dial: arrow keys and assistive-technology steps turn it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
paaspaas00andClaude Opus 5.5 committed 2026-10-21 13:03:03 +02:00
1 parent 161b359c37
commit 5544efdbdc
1 file changed
+94 -4
+94 -4
View File
@@ -217,8 +217,11 @@ fn angle(ui: &mut egui::Ui, d: &mut Dialog, path: &[&str], enabled: bool, global
}
/// InDesign's angle dial: a circle with a radius pointing where the light comes from (0° to the
/// right, counter-clockwise). A click or a drag on it points it at the pointer.
/// right, counter-clockwise). A click or a drag on it points it at the pointer. Focused, the
/// arrow keys turn it a degree (Shift: 10°) — Right and Up counter-clockwise — and assistive
/// technology can step it or set its value, as with a slider.
fn angle_dial(ui: &mut egui::Ui, deg: f64, enabled: bool) -> Option<f64> {
use egui::accesskit::{Action, ActionData};
let sense = if enabled { egui::Sense::click_and_drag() } else { egui::Sense::hover() };
let (r, resp) = ui.allocate_exact_size(egui::vec2(26.0, 26.0), sense);
resp.widget_info(|| egui::WidgetInfo::slider(enabled, deg, "Angle"));
@@ -227,10 +230,64 @@ fn angle_dial(ui: &mut egui::Ui, deg: f64, enabled: bool) -> Option<f64> {
ui.painter().circle_stroke(c, rad, egui::Stroke::new(1.2, col));
let a = deg.to_radians() as f32;
ui.painter().line_segment([c, c + egui::vec2(a.cos(), -a.sin()) * rad], egui::Stroke::new(1.4, col));
if enabled && (resp.dragged() || resp.clicked()) {
return resp.interact_pointer_pos().and_then(|p| dial_angle(c, p));
if resp.has_focus() {
ui.painter().circle_stroke(c, rad + 2.0, ui.visuals().selection.stroke);
}
None
if !enabled {
return None;
}
ui.ctx().accesskit_node_builder(resp.id, |node| {
node.set_min_numeric_value(-180.0);
node.set_max_numeric_value(180.0);
node.set_numeric_value_step(1.0);
node.add_action(Action::SetValue);
node.add_action(Action::Increment);
node.add_action(Action::Decrement);
});
let mut next = None;
let mut steps = 0i64;
if resp.has_focus() {
// The arrows turn the dial instead of moving the focus.
ui.memory_mut(|m| {
m.set_focus_lock_filter(resp.id, egui::EventFilter { horizontal_arrows: true, vertical_arrows: true, ..Default::default() })
});
ui.input(|i| {
for e in &i.events {
if let egui::Event::Key { key, pressed: true, modifiers, .. } = e {
let dir = match key {
egui::Key::ArrowRight | egui::Key::ArrowUp => 1,
egui::Key::ArrowLeft | egui::Key::ArrowDown => -1,
_ => 0,
};
steps += dir * if modifiers.shift { 10 } else { 1 };
}
}
});
}
ui.input(|i| {
steps += i.num_accesskit_action_requests(resp.id, Action::Increment) as i64;
steps -= i.num_accesskit_action_requests(resp.id, Action::Decrement) as i64;
for request in i.accesskit_action_requests(resp.id, Action::SetValue) {
if let Some(ActionData::NumericValue(v)) = request.data {
next = Some(v);
}
}
});
if steps != 0 {
next = Some(deg + steps as f64);
}
if (resp.dragged() || resp.clicked())
&& let Some(a) = resp.interact_pointer_pos().and_then(|p| dial_angle(c, p))
{
next = Some(a);
}
next.filter(|v| v.is_finite()).map(wrap_angle)
}
/// `deg` turned into the dial's range, −180 (exclusive) to 180°.
fn wrap_angle(deg: f64) -> f64 {
let a = (deg + 180.0).rem_euclid(360.0) - 180.0;
if a <= -180.0 { 180.0 } else { a }
}
/// The angle a dial at `c` points at for the pointer at `p`, in whole degrees (−180 to 180,
@@ -769,6 +826,39 @@ mod tests {
assert_eq!(dial_angle(c, egui::pos2(101.0, 100.0)), None, "too close to the centre");
}
/// Without a pointer: Tab focuses the dial, the arrows turn it (Shift: 10°), wrapping at 180°.
#[test]
fn the_angle_dial_turns_with_the_keyboard() {
let ctx = egui::Context::default();
let key = |key, shift| egui::Event::Key {
key,
physical_key: None,
pressed: true,
repeat: false,
modifiers: if shift { egui::Modifiers::SHIFT } else { egui::Modifiers::NONE },
};
let mut deg = 175.0;
let frame = |events: Vec<egui::Event>, deg: &mut f64| {
let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
if let Some(a) = angle_dial(ui, *deg, true) {
*deg = a;
}
});
out.textures_delta.clear();
};
frame(vec![], &mut deg);
frame(vec![key(egui::Key::Tab, false)], &mut deg);
frame(vec![key(egui::Key::ArrowRight, false)], &mut deg);
assert_eq!(deg, 176.0);
frame(vec![key(egui::Key::ArrowUp, true)], &mut deg);
assert_eq!(deg, -174.0, "past 180° it comes round to −174°");
frame(vec![key(egui::Key::ArrowLeft, true)], &mut deg);
frame(vec![key(egui::Key::ArrowDown, false)], &mut deg);
assert_eq!(deg, 175.0);
assert_eq!(wrap_angle(-180.0), 180.0);
assert_eq!(wrap_angle(540.0), 180.0);
}
#[test]
fn offsets_follow_distance_and_angle_as_in_indesign() {
let (mut app, _) = app_with_frame();