mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-14 14:32:31 +00:00
ui: apply touchpad scroll multiplier once per event across hidden ticks (#6146)
This commit is contained in:
1 parent
fa43ecc4bf
commit
1c3256ab65
7 files changed
+109
-6
No files matched your search
@@ -114,6 +114,8 @@ impl WordApp {
|
||||
synthetic: Vec::new(),
|
||||
synthetic_pending: false,
|
||||
synthetic_injected: 0,
|
||||
raw_scroll_events_scaled: 0,
|
||||
last_scaled_scroll_events: Vec::new(),
|
||||
styled: false,
|
||||
fonts_hans: false,
|
||||
fonts_korean: false,
|
||||
|
||||
@@ -8,6 +8,8 @@ impl WordApp {
|
||||
// a layout anyone will see. The shot's `issued` flag makes repeated final passes harmless.
|
||||
if !ctx.will_discard() {
|
||||
control::issue_screenshots(self, ctx);
|
||||
self.raw_scroll_events_scaled = 0;
|
||||
self.last_scaled_scroll_events.clear();
|
||||
}
|
||||
// The raw input was consumed by this host frame's passes; only a final UI pass proves
|
||||
// the step is done. A discarded pass keeps the queue for the next UI frame.
|
||||
|
||||
@@ -24,11 +24,15 @@ impl WordApp {
|
||||
if cfg!(target_os = "macos") {
|
||||
control_click_as_secondary(&mut raw.events, &mut self.control_click);
|
||||
}
|
||||
self.ui.touchpad_scroll_speed.apply_input_scroll(raw, self.ui.scroll_speed);
|
||||
self.apply_raw_input_scroll(raw);
|
||||
if self.synthetic_pending || self.synthetic.is_empty() || self.fonts_frames < 2 || !self.control_input_is_roots(ctx) {
|
||||
self.record_scaled_scroll_events(raw);
|
||||
return;
|
||||
}
|
||||
let Some((events, n)) = self.peek_synthetic() else { return };
|
||||
let Some((events, n)) = self.peek_synthetic() else {
|
||||
self.record_scaled_scroll_events(raw);
|
||||
return;
|
||||
};
|
||||
if events.iter().any(|e| matches!(e, egui::Event::Key { .. } | egui::Event::Text(_))) {
|
||||
// Control input belongs to this window's remembered widget even while another app
|
||||
// is active. Give only this input frame egui focus, without requesting OS activation.
|
||||
@@ -41,6 +45,28 @@ impl WordApp {
|
||||
raw.events.extend(events);
|
||||
self.synthetic_pending = true;
|
||||
self.synthetic_injected = n;
|
||||
self.record_scaled_scroll_events(raw);
|
||||
}
|
||||
|
||||
/// Scales point-wheel events in the incoming [`egui::RawInput`] by the touchpad / wheel speed multiplier.
|
||||
/// When eframe or web runners replay an unconsumed [`egui::RawInput`] across hidden logic ticks,
|
||||
/// already-scaled events in the pending prefix are skipped so the multiplier is applied exactly once per event (#6146).
|
||||
fn apply_raw_input_scroll(&mut self, raw: &mut egui::RawInput) {
|
||||
let previously_scaled = self.raw_scroll_events_scaled;
|
||||
let is_replayed = previously_scaled > 0
|
||||
&& raw.events.len() >= previously_scaled
|
||||
&& raw.events.get(..previously_scaled) == Some(&self.last_scaled_scroll_events[..]);
|
||||
let start_idx = if is_replayed { previously_scaled } else { 0 };
|
||||
if let Some(new_events) = raw.events.get_mut(start_idx..) {
|
||||
self.ui.touchpad_scroll_speed.apply_events_scroll(new_events, self.ui.scroll_speed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Records the current state of scaled events in the raw input batch so replay across hidden ticks
|
||||
/// does not compound multipliers.
|
||||
fn record_scaled_scroll_events(&mut self, raw: &egui::RawInput) {
|
||||
self.raw_scroll_events_scaled = raw.events.len();
|
||||
self.last_scaled_scroll_events = raw.events.clone();
|
||||
}
|
||||
|
||||
/// Whether this pass's input came from the control channel (`ui.click`, `ui.key`, `ui.drag`,
|
||||
|
||||
@@ -683,6 +683,11 @@ pub struct WordApp {
|
||||
synthetic_pending: bool,
|
||||
/// How many events of `synthetic` the pending batch contains.
|
||||
synthetic_injected: usize,
|
||||
/// Number of raw input events whose scroll delta has already been scaled in the pending host batch.
|
||||
pub(crate) raw_scroll_events_scaled: usize,
|
||||
/// Exact snapshot of the raw input events scaled during the current pending batch, to avoid
|
||||
/// compounding scroll multipliers when eframe replays unconsumed RawInput across hidden logic ticks.
|
||||
pub(crate) last_scaled_scroll_events: Vec<egui::Event>,
|
||||
/// macOS: the primary button went down with Control, so it is being read as the secondary.
|
||||
control_click: bool,
|
||||
/// Badge rects recorded during layout for the current keytip phase (cleared by `logic`, painted by `show`).
|
||||
|
||||
@@ -88,17 +88,16 @@ impl ScrollSpeed {
|
||||
NORMAL_TOUCHPAD_FACTOR * self.factor()
|
||||
}
|
||||
|
||||
/// Scales this frame's point scrolling by [`Self::touchpad_factor`].
|
||||
/// Scales point scrolling events in the given slice by [`Self::touchpad_factor`].
|
||||
/// On wasm32, browser mouse wheels and touchpads arrive as [`egui::MouseWheelUnit::Point`],
|
||||
/// so the user-selected `wheel_speed` is applied there directly (#6146).
|
||||
/// Call this before egui or canvas reads raw input.
|
||||
/// Ctrl/⌘-wheel zoom is left alone so it zooms at egui's own rate.
|
||||
pub fn apply_input_scroll(self, raw: &mut egui::RawInput, wheel_speed: ScrollSpeed) {
|
||||
pub fn apply_events_scroll(self, events: &mut [egui::Event], wheel_speed: ScrollSpeed) {
|
||||
let factor = if cfg!(target_arch = "wasm32") { wheel_speed.factor() } else { self.touchpad_factor() };
|
||||
if (factor - 1.0).abs() < 1e-4 {
|
||||
return;
|
||||
}
|
||||
for event in &mut raw.events {
|
||||
for event in events {
|
||||
if let egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Point, delta, modifiers, .. } = event
|
||||
&& !modifiers.command
|
||||
&& !modifiers.ctrl
|
||||
@@ -109,6 +108,15 @@ impl ScrollSpeed {
|
||||
}
|
||||
}
|
||||
|
||||
/// Scales this frame's point scrolling by [`Self::touchpad_factor`].
|
||||
/// On wasm32, browser mouse wheels and touchpads arrive as [`egui::MouseWheelUnit::Point`],
|
||||
/// so the user-selected `wheel_speed` is applied there directly (#6146).
|
||||
/// Call this before egui or canvas reads raw input.
|
||||
/// Ctrl/⌘-wheel zoom is left alone so it zooms at egui's own rate.
|
||||
pub fn apply_input_scroll(self, raw: &mut egui::RawInput, wheel_speed: ScrollSpeed) {
|
||||
self.apply_events_scroll(&mut raw.events, wheel_speed);
|
||||
}
|
||||
|
||||
/// Scales this frame's touchpad (point) scrolling by [`Self::touchpad_factor`].
|
||||
/// Call this before egui or canvas reads raw input.
|
||||
/// Ctrl/⌘-wheel zoom is left alone so it zooms at egui's own rate.
|
||||
|
||||
@@ -8219,3 +8219,61 @@ fn touchpad_and_mouse_scroll_speed_scaling_and_preferences() {
|
||||
assert_eq!(restored.touchpad_scroll_speed, crate::scroll::ScrollSpeed::Fast);
|
||||
assert_eq!(restored.scroll_speed, crate::scroll::ScrollSpeed::Fast);
|
||||
}
|
||||
|
||||
/// Issue #6146 / PR #6167: unconsumed raw input is replayed by eframe across hidden logic ticks.
|
||||
/// The touchpad scroll multiplier must be applied once per event, never compounded across replays.
|
||||
#[test]
|
||||
fn touchpad_point_scrolling_is_scaled_once_across_hidden_logic_ticks() {
|
||||
let mut a = app();
|
||||
let ctx = egui::Context::default();
|
||||
let wheel_event = |unit, y: f32| egui::Event::MouseWheel {
|
||||
unit,
|
||||
delta: egui::vec2(0.0, y),
|
||||
phase: egui::TouchPhase::Move,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
};
|
||||
|
||||
let mut raw = egui::RawInput { events: vec![wheel_event(egui::MouseWheelUnit::Point, -10.0)], ..Default::default() };
|
||||
|
||||
// 1. First raw_input_hook call scales the point delta by the touchpad factor.
|
||||
a.raw_input_hook(&ctx, &mut raw);
|
||||
let expected = -10.0 * a.ui.touchpad_scroll_speed.touchpad_factor();
|
||||
assert_eq!(raw.events[0], wheel_event(egui::MouseWheelUnit::Point, expected));
|
||||
|
||||
// 2. Hidden logic tick: eframe saves raw into pending_raw_input and invokes the hook again
|
||||
// on the next tick without a UI pass having consumed it. The delta must not compound.
|
||||
a.logic(&ctx);
|
||||
a.raw_input_hook(&ctx, &mut raw);
|
||||
if let egui::Event::MouseWheel { delta, .. } = &raw.events[0] {
|
||||
assert_eq!(delta.y, expected, "hidden logic tick replay must not compound the scroll delta");
|
||||
}
|
||||
|
||||
// 3. Appending a new event during a subsequent hidden tick: older events stay scaled once,
|
||||
// and only the new event is scaled.
|
||||
raw.events.push(wheel_event(egui::MouseWheelUnit::Point, -5.0));
|
||||
a.logic(&ctx);
|
||||
a.raw_input_hook(&ctx, &mut raw);
|
||||
let expected_new = -5.0 * a.ui.touchpad_scroll_speed.touchpad_factor();
|
||||
if let egui::Event::MouseWheel { delta, .. } = &raw.events[0] {
|
||||
assert_eq!(delta.y, expected, "initial event still scaled once");
|
||||
}
|
||||
if let egui::Event::MouseWheel { delta, .. } = &raw.events[1] {
|
||||
assert_eq!(delta.y, expected_new, "new event is scaled once");
|
||||
}
|
||||
|
||||
// 4. A UI pass consumes the input, acknowledging the frame.
|
||||
ctx.run_ui(raw, |ui| {
|
||||
a.logic(ui.ctx());
|
||||
a.ui(ui);
|
||||
})
|
||||
.drop_without_applying_deltas();
|
||||
|
||||
// 5. Subsequent fresh frame scales fresh input once.
|
||||
let mut fresh = egui::RawInput { events: vec![wheel_event(egui::MouseWheelUnit::Point, -20.0)], ..Default::default() };
|
||||
a.raw_input_hook(&ctx, &mut fresh);
|
||||
let expected_fresh = -20.0 * a.ui.touchpad_scroll_speed.touchpad_factor();
|
||||
if let egui::Event::MouseWheel { delta, .. } = &fresh.events[0] {
|
||||
assert_eq!(delta.y, expected_fresh, "fresh input is scaled once");
|
||||
}
|
||||
}
|
||||
@@ -178,6 +178,8 @@ impl WordApp {
|
||||
synthetic: _,
|
||||
synthetic_pending: _,
|
||||
synthetic_injected: _,
|
||||
raw_scroll_events_scaled: _,
|
||||
last_scaled_scroll_events: _,
|
||||
// The current command's origin, set and cleared around it.
|
||||
ime_origin: _,
|
||||
styled: _,
|
||||
|
||||
Reference in new issue
Block a user