mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
This commit is contained in:
1 parent
3e437b7bd4
commit
bb8fbd6b67
3 files changed
+131
-12
No files matched your search
@@ -17,6 +17,9 @@ const MAX_DISPLACEMENT: f32 = 1_000_000.0;
|
||||
pub(crate) struct AutoScroll {
|
||||
anchor: Option<Pos2>,
|
||||
organize: bool,
|
||||
/// Consecutive or total frames spent past [`DEAD_ZONE`] while the initiating middle press
|
||||
/// remained held (`>= 2` distinguishes a held scroll from a quick click).
|
||||
held_frames_past_dead_zone: u8,
|
||||
/// Own a cancelling click through its release, so it cannot also edit page content.
|
||||
cancel_button: Option<PointerButton>,
|
||||
block_input: bool,
|
||||
@@ -36,6 +39,7 @@ impl AutoScroll {
|
||||
|
||||
pub(crate) fn cancel(&mut self) {
|
||||
self.anchor = None;
|
||||
self.held_frames_past_dead_zone = 0;
|
||||
self.drag = None;
|
||||
self.cancel_button = None;
|
||||
self.block_input = false;
|
||||
@@ -59,7 +63,7 @@ impl AutoScroll {
|
||||
|
||||
fn update_latched(&mut self, ui: &egui::Ui, viewport: egui::Rect, organize: bool) -> Vec2 {
|
||||
let ctx = ui.ctx();
|
||||
let (pointer, middle_press, middle_down, middle_released, cancel, cancel_button, dt) = ctx.input(|i| {
|
||||
let (pointer, middle_press, middle_down, middle_released, has_release_event, cancel, cancel_button, dt) = ctx.input(|i| {
|
||||
let cancel_button = [PointerButton::Primary, PointerButton::Secondary, PointerButton::Extra1, PointerButton::Extra2]
|
||||
.into_iter()
|
||||
.find(|button| i.pointer.button_pressed(*button));
|
||||
@@ -72,6 +76,7 @@ impl AutoScroll {
|
||||
}),
|
||||
i.pointer.button_down(PointerButton::Middle),
|
||||
i.pointer.button_released(PointerButton::Middle),
|
||||
i.events.iter().any(|e| matches!(e, Event::PointerButton { button: PointerButton::Middle, pressed: false, .. })),
|
||||
!i.focused
|
||||
|| i.key_pressed(Key::Escape)
|
||||
|| cancel_button.is_some()
|
||||
@@ -80,7 +85,8 @@ impl AutoScroll {
|
||||
i.stable_dt,
|
||||
)
|
||||
});
|
||||
self.block_input = self.active() || self.cancel_button.is_some() || middle_press.is_some() || middle_down || middle_released;
|
||||
self.block_input =
|
||||
self.active() || self.cancel_button.is_some() || middle_press.is_some() || middle_down || middle_released || has_release_event;
|
||||
if let Some(button) = self.cancel_button {
|
||||
if !ctx.input(|i| i.pointer.button_down(button)) {
|
||||
self.cancel_button = None;
|
||||
@@ -106,13 +112,23 @@ impl AutoScroll {
|
||||
{
|
||||
self.anchor = Some(pressed_at);
|
||||
self.organize = organize;
|
||||
self.held_frames_past_dead_zone = 0;
|
||||
}
|
||||
}
|
||||
let (Some(anchor), Some(pointer)) = (self.anchor, pointer) else { return Vec2::ZERO };
|
||||
let displacement = pointer.y - anchor.y;
|
||||
let past_dead_zone = displacement.is_finite() && displacement.abs() > DEAD_ZONE;
|
||||
if middle_down && !has_release_event && past_dead_zone {
|
||||
self.held_frames_past_dead_zone = self.held_frames_past_dead_zone.saturating_add(1);
|
||||
}
|
||||
if (middle_released || has_release_event) && self.held_frames_past_dead_zone >= 2 {
|
||||
self.cancel();
|
||||
self.block_input = true;
|
||||
return Vec2::ZERO;
|
||||
}
|
||||
// Cap the elapsed time too: returning from an idle/hidden window must never jump pages.
|
||||
let delta = scroll_delta(displacement, dt);
|
||||
if displacement.is_finite() && displacement.abs() > DEAD_ZONE {
|
||||
if past_dead_zone {
|
||||
// Continuous redraws let stable_dt use measured frame time. Delayed redraws
|
||||
// instead use predicted_dt, which can make speed depend on the actual frame rate.
|
||||
ctx.request_repaint();
|
||||
@@ -124,6 +140,7 @@ impl AutoScroll {
|
||||
/// blocked from the press through the release, so the drag cannot also draw, select or move.
|
||||
fn update_drag(&mut self, ui: &egui::Ui, viewport: egui::Rect) -> Vec2 {
|
||||
self.anchor = None;
|
||||
self.held_frames_past_dead_zone = 0;
|
||||
self.cancel_button = None;
|
||||
let ctx = ui.ctx();
|
||||
let (pointer, middle_press, middle_down, middle_released, focused) = ctx.input(|i| {
|
||||
@@ -319,6 +336,103 @@ mod tests {
|
||||
assert_eq!(latched_by(10.0, true), Vec2::ZERO, "the dead zone still holds still");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn holding_middle_button_to_auto_scroll_turns_off_on_release() {
|
||||
let ctx = Context::default();
|
||||
let mut scroll = AutoScroll::default();
|
||||
let p = egui::pos2(100.0, 100.0);
|
||||
let far = p + vec2(0.0, 60.0);
|
||||
frame(&ctx, &mut scroll, vec![], true, (true, false), |_, _| {});
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(p), middle(p, true)], true, (true, false), |s, d| {
|
||||
assert!(s.active() && s.blocks_input());
|
||||
assert_eq!(d, Vec2::ZERO);
|
||||
});
|
||||
for _ in 0..2 {
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(far)], true, (true, false), |s, d| {
|
||||
assert!(s.active() && s.blocks_input());
|
||||
assert!(d.y < 0.0, "holding and moving past the dead zone scrolls: {d:?}");
|
||||
});
|
||||
}
|
||||
// Primary case: releasing while still past the dead zone stops auto-scroll immediately.
|
||||
frame(&ctx, &mut scroll, vec![middle(far, false)], true, (true, false), |s, d| {
|
||||
assert!(!s.active(), "releasing at +60pt after holding must turn auto-scroll off");
|
||||
assert!(s.blocks_input(), "the release frame itself still blocks page tools");
|
||||
assert_eq!(d, Vec2::ZERO);
|
||||
});
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(far)], true, (true, false), |s, d| {
|
||||
assert!(!s.active() && !s.blocks_input());
|
||||
assert_eq!(d, Vec2::ZERO);
|
||||
});
|
||||
|
||||
// Returning inside the dead zone before releasing still counts as a held scroll gesture.
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(p), middle(p, true)], true, (true, false), |s, _| assert!(s.active()));
|
||||
for _ in 0..2 {
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(far)], true, (true, false), |s, d| assert!(s.active() && d.y < 0.0));
|
||||
}
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(p + vec2(0.0, 5.0)), middle(p + vec2(0.0, 5.0), false)], true, (true, false), |s, d| {
|
||||
assert!(!s.active(), "releasing after scrolling while held must turn auto-scroll off");
|
||||
assert!(s.blocks_input());
|
||||
assert_eq!(d, Vec2::ZERO);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_click_released_within_dead_zone_stays_latched() {
|
||||
let ctx = Context::default();
|
||||
let mut scroll = AutoScroll::default();
|
||||
let p = egui::pos2(100.0, 100.0);
|
||||
let far = p + vec2(0.0, 60.0);
|
||||
frame(&ctx, &mut scroll, vec![], true, (true, false), |_, _| {});
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(p), middle(p, true)], true, (true, false), |s, _| assert!(s.active()));
|
||||
frame(
|
||||
&ctx,
|
||||
&mut scroll,
|
||||
vec![Event::PointerMoved(p + vec2(0.0, DEAD_ZONE)), middle(p + vec2(0.0, DEAD_ZONE), false)],
|
||||
true,
|
||||
(true, false),
|
||||
|s, d| {
|
||||
assert!(s.active(), "releasing within the dead zone keeps auto-scroll latched");
|
||||
assert_eq!(d, Vec2::ZERO);
|
||||
},
|
||||
);
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(far)], true, (true, false), |s, d| {
|
||||
assert!(s.active());
|
||||
assert!(d.y < 0.0);
|
||||
});
|
||||
scroll.cancel();
|
||||
|
||||
// Pointer movement after middle-button release in the same frame does not count as held movement.
|
||||
frame(&ctx, &mut scroll, vec![Event::PointerMoved(p), middle(p, true)], true, (true, false), |s, _| assert!(s.active()));
|
||||
frame(&ctx, &mut scroll, vec![middle(p, false), Event::PointerMoved(far)], true, (true, false), |s, d| {
|
||||
assert!(s.active(), "movement after release in the same frame keeps auto-scroll latched");
|
||||
assert!(d.y < 0.0);
|
||||
});
|
||||
scroll.cancel();
|
||||
|
||||
// A press, move, and release arriving in a single frame is a click at the initial press position,
|
||||
// and stays latched across subsequent frames when egui delivers its deferred button_released state
|
||||
// or runs multiple layout passes within the activation frame.
|
||||
frame(
|
||||
&ctx,
|
||||
&mut scroll,
|
||||
vec![Event::PointerMoved(p), middle(p, true), Event::PointerMoved(far), middle(far, false)],
|
||||
true,
|
||||
(true, false),
|
||||
|s, d| {
|
||||
assert!(s.active(), "single-frame click and move latches auto-scroll");
|
||||
assert!(d.y < 0.0);
|
||||
},
|
||||
);
|
||||
frame(&ctx, &mut scroll, vec![], true, (true, false), |s, d| {
|
||||
assert!(s.active(), "deferred button_released in the next frame must keep auto-scroll latched");
|
||||
assert!(d.y < 0.0);
|
||||
});
|
||||
frame(&ctx, &mut scroll, vec![], true, (true, false), |s, d| {
|
||||
assert!(s.active(), "subsequent frames after deferred release must keep auto-scroll latched");
|
||||
assert!(d.y < 0.0);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_drag_pans_one_to_one_and_owns_the_button_until_release() {
|
||||
let ctx = Context::default();
|
||||
|
||||
@@ -2790,16 +2790,12 @@ fn a_middle_drag_with_a_drawing_tool_scrolls_instead_of_drawing() {
|
||||
assert!(!s.views[0].middle_panning(), "releasing the wheel ends the pan");
|
||||
let moved = s.views[0].page_screen_rect(0).expect("page 1 on screen").top();
|
||||
if cfg!(target_os = "linux") {
|
||||
// Linux latches auto-scroll instead of panning with the drag.
|
||||
assert!(s.views[0].auto_scrolling());
|
||||
// Linux auto-scrolls while the wheel is held and stops on release.
|
||||
assert!(!s.views[0].auto_scrolling());
|
||||
} else {
|
||||
assert!((moved - (top - 120.0)).abs() < 1.0, "the page follows the pointer: {top} -> {moved}");
|
||||
}
|
||||
// The tool still draws with the primary button afterwards.
|
||||
if cfg!(target_os = "linux") {
|
||||
h.key_press(egui::Key::Escape);
|
||||
h.run_steps(2);
|
||||
}
|
||||
drag_pt(&mut h, (60.0, 120.0), (200.0, 80.0));
|
||||
let kinds: Vec<String> = comments(&h).into_iter().map(|a| a.subtype).collect();
|
||||
assert_eq!(kinds, ["Ink"]);
|
||||
|
||||
@@ -665,13 +665,21 @@ fn moving_before_middle_button_release_keeps_scrolling_without_starting_page_too
|
||||
ok(&mut h, &c, "ui.drag", json!({ "from": [p.x, p.y], "to": [p.x, p.y + 60.0], "steps": 12, "button": "middle" }));
|
||||
h.run_steps(2);
|
||||
assert!(h.state().views[0].page_screen_rect(0).unwrap().top() < top - 20.0);
|
||||
assert!(h.state().views[0].auto_scrolling(), "release keeps scrolling toggled on even after movement");
|
||||
assert!(!h.state().views[0].auto_scrolling(), "releasing after holding and scrolling turns auto-scroll off");
|
||||
let released = h.state().views[0].page_screen_rect(0).unwrap().top();
|
||||
h.run_steps(8);
|
||||
assert!(h.state().views[0].page_screen_rect(0).unwrap().top() < released - 30.0, "scrolling continues with no button held");
|
||||
assert_eq!(h.state().views[0].page_screen_rect(0).unwrap().top(), released, "no drift after releasing a held middle drag");
|
||||
assert!(h.state().views[0].crop_drag.is_none(), "the Crop tool must not receive a wheel drag");
|
||||
assert!(h.state().dialog.is_none());
|
||||
assert!(!h.state().session.get(h.state().views[0].id).unwrap().dirty);
|
||||
|
||||
// Small movement within the dead zone before release still latches auto-scroll on.
|
||||
ok(&mut h, &c, "ui.drag", json!({ "from": [p.x, p.y], "to": [p.x, p.y + 10.0], "steps": 4, "button": "middle" }));
|
||||
assert!(h.state().views[0].auto_scrolling(), "release within the dead zone keeps scrolling toggled on");
|
||||
ok(&mut h, &c, "ui.move", json!({ "x": p.x, "y": p.y + 60.0 }));
|
||||
h.run_steps(8);
|
||||
assert!(h.state().views[0].page_screen_rect(0).unwrap().top() < released - 30.0, "scrolling continues with no button held");
|
||||
assert!(h.state().views[0].crop_drag.is_none(), "the Crop tool must not receive a wheel drag");
|
||||
ok(&mut h, &c, "ui.click", json!({ "x": p.x, "y": p.y + 60.0, "button": "middle" }));
|
||||
assert!(!h.state().views[0].auto_scrolling(), "the next middle click toggles scrolling off");
|
||||
}
|
||||
@@ -886,7 +894,8 @@ fn organize_pages_supports_autoscroll_without_selecting_or_reordering_pages() {
|
||||
assert!(h.state().views[0].selected.is_empty());
|
||||
assert!(h.state().views[0].org_drag.is_none());
|
||||
assert!(!h.state().session.get(h.state().views[0].id).unwrap().dirty);
|
||||
assert!(h.state().views[0].auto_scrolling(), "releasing the wheel leaves the grid scrolling on");
|
||||
assert!(!h.state().views[0].auto_scrolling(), "releasing a held wheel stops the grid scrolling");
|
||||
start_autoscroll(&mut h, &c);
|
||||
h.state_mut().views[0].organize = false;
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().views[0].auto_scrolling(), "changing canvas mode cancels the gesture");
|
||||
|
||||
Reference in new issue
Block a user