# Conflicts:
#	crates/ui-egui/src/tests_masking.rs
This commit is contained in:
Claude committed 2026-10-11 11:41:37 +00:00
commit 814e35b103
2 files changed
+77 -2

No files matched your search

+21
View File
@@ -1211,4 +1211,25 @@ fn color_range_pick_button_samples_the_photo() {
click(&mut h, "button:colorPick0");
assert!(h.app.ui.tool.is_empty());
h.settle(SETTLE);
#[test]
fn a_selected_develop_slider_accepts_wheel_nudges() {
let mut h = detail("panel.edit");
h.settle(SETTLE);
let name = "slider:light.exposure";
let panel = h.app.widgets.iter().find(|(id, _)| id == "panel:right_panel").unwrap().1;
let track = h.app.widgets.iter().find(|(id, _)| id == name).unwrap().1;
assert!(panel.contains(track.center()), "slider must be visible before interaction");
let result = h.request("ui.clickWidget", json!({"id": name, "fx": 0.5}), T);
assert_eq!(result["ok"], true, "{result}");
let before = lightcraft_develop::controls::get(&develop(&h), "light.exposure");
// A wheel event over the selected control adjusts exposure instead of
// scrolling the right sidebar (an unselected slider leaves scrolling alone).
let result = h.request("ui.scroll", json!({"dy": 120.0}), T);
assert_eq!(result["ok"], true, "{result}");
h.step();
let after = lightcraft_develop::controls::get(&develop(&h), "light.exposure");
assert!(after > before, "selected exposure did not increase: {before} -> {after}");
}
+56 -2
View File
@@ -183,6 +183,11 @@ pub fn slider(ui: &mut Ui, spec: &ControlSpec, value: f64, enabled: bool, label_
let track_rect = Rect::from_min_max(pos2(row.left() + pad_l, row.top() + 22.0), pos2(row.right() - pad_r, row.top() + 40.0));
let id = ui.id().with(spec.id);
let resp = ui.interact(track_rect.expand2(vec2(8.0, 2.0)), id, if enabled { Sense::click_and_drag() } else { Sense::hover() });
// A click selects the slider for arrow/wheel adjustments. Keep scroll-to-adjust
// opt-in so hovering an unfocused slider still scrolls the containing panel.
if enabled && (resp.clicked() || resp.drag_started()) {
resp.request_focus();
}
// screen readers: a slider named after its control, with its value
let label_text = crate::i18n::tr(label_override.unwrap_or(spec.label)).to_string();
resp.widget_info(|| egui::WidgetInfo::slider(enabled, value, label_text.clone()));
@@ -272,8 +277,8 @@ pub fn slider(ui: &mut Ui, spec: &ControlSpec, value: f64, enabled: bool, label_
if resp.drag_stopped() {
out.drag_stopped = true;
}
// ↑ / ↓ while the pointer rests on the row nudge the value (⇧: five times as much)
if enabled && out.value.is_none() && typing.is_none() && ui.rect_contains_pointer(row) {
// Arrow keys work with either a hovered or a focused slider.
if enabled && out.value.is_none() && typing.is_none() && (ui.rect_contains_pointer(row) || resp.has_focus()) {
let (up, down, shift) = ui.input_mut(|i| {
let shift = i.modifiers.shift;
let m = if shift { egui::Modifiers::SHIFT } else { egui::Modifiers::NONE };
@@ -289,6 +294,34 @@ pub fn slider(ui: &mut Ui, spec: &ControlSpec, value: f64, enabled: bool, label_
}
}
}
// Wheel nudges require an explicitly selected slider, unlike regular panel scrolling.
// Consume only the vertical component so a trackpad's horizontal scroll still works.
let wheel_id = id.with("wheel-rem");
if !resp.has_focus() {
ui.data_mut(|m| m.remove::<f32>(wheel_id));
} else if enabled && typing.is_none() && out.value.is_none() && ui.rect_contains_pointer(row) {
let delta = ui.input_mut(|i| {
let dy = i.smooth_scroll_delta.y;
if dy != 0.0 {
i.smooth_scroll_delta.y = 0.0;
}
dy
});
if delta != 0.0 {
let mut remainder = ui.data(|m| m.get_temp::<f32>(wheel_id)).unwrap_or(0.0);
let steps = wheel_nudges(&mut remainder, delta);
ui.data_mut(|m| m.insert_temp(wheel_id, remainder));
if steps != 0.0 {
let nv = nudged(spec, value, steps);
if (nv - value).abs() > 1e-12 {
out.value = Some(nv);
out.drag_started = true;
out.drag_stopped = true;
v = nv;
}
}
}
}
}
// paint
let hovered = resp.hovered() || resp.dragged();
@@ -320,6 +353,16 @@ pub fn slider(ui: &mut Ui, spec: &ControlSpec, value: f64, enabled: bool, label_
out
}
/// Convert vertical wheel movement to bounded slider steps, keeping sub-notch
/// trackpad movement between frames without amplifying a single large event.
fn wheel_nudges(remainder: &mut f32, delta: f32) -> f64 {
const POINTS_PER_STEP: f32 = 40.0;
let total = (*remainder + delta).clamp(-400.0, 400.0);
let steps = (total / POINTS_PER_STEP).trunc();
*remainder = total - steps * POINTS_PER_STEP;
f64::from(steps)
}
/// `value` moved by `steps` keyboard nudges: one nudge is about 1/200 of the range, a whole
/// number of the control's steps (exposure 0.05, most sliders 1, temperature 50 K).
pub fn nudged(spec: &ControlSpec, value: f64, steps: f64) -> f64 {
@@ -546,6 +589,17 @@ mod tests {
assert_eq!(nudged(&spec("light.contrast", -100.0, 100.0, 1.0), 99.0, 5.0), 100.0, "clamped");
}
#[test]
fn wheel_steps_accumulate_small_movements_and_preserve_direction() {
let mut remainder = 0.0;
assert_eq!(wheel_nudges(&mut remainder, 15.0), 0.0);
assert_eq!(wheel_nudges(&mut remainder, 25.0), 1.0);
assert_eq!(remainder, 0.0);
assert_eq!(wheel_nudges(&mut remainder, -100.0), -2.0);
assert_eq!(wheel_nudges(&mut remainder, -20.0), -1.0);
assert_eq!(remainder, 0.0);
}
/// Typed values (issue #322): signs, a decimal comma, units; on the control's steps, in range.
#[test]
fn typed_values_are_stepped_and_clamped() {