mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-13 05:42:31 +00:00
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:
1 parent
161b359c37
commit
5544efdbdc
1 file changed
+94
-4
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user