use std::collections::HashMap;
use frust_core::{EventCtx, EventResult, InputEvent, PointerPhase, VelocityTracker};
use kurbo::Point;
use super::navigator::{ActiveTransition, NavigatorWidget};
use super::options::BackPolicy;
use super::transition::{PageTransition, TransitionDriver, make_driver, settle_driver};
pub(super) const EDGE_SWIPE_ZONE_DP: f64 = 20.0;
pub(super) const EDGE_SWIPE_COMMIT_PROGRESS: f64 = 0.5;
const EDGE_SWIPE_FLING_VELOCITY: f64 = 1300.0;
pub(super) struct EdgeSwipe {
pub(super) armed: bool,
pub(super) active: bool,
pub(super) down_start: Point,
pub(super) tracker: VelocityTracker,
pub(super) inner_claimed: bool,
}
impl EdgeSwipe {
pub(super) fn new() -> Self {
Self {
armed: false,
active: false,
down_start: Point::ZERO,
tracker: VelocityTracker::new(),
inner_claimed: false,
}
}
}
impl<State: 'static> NavigatorWidget<State> {
pub(super) fn begin_interactive_pop(&mut self, initial_progress: f64) {
debug_assert!(self.pages.len() > 1, "steal requires a poppable stack");
debug_assert!(
self.transition.is_none(),
"steal requires no active transition"
);
let outgoing_focused = self.top_pod_focused();
self.cancel_top();
let popped = self.pages.pop().expect("depth > 1 checked before steal");
popped.reach.set(false);
let from_depth = self.pages.len() + 1;
let spec = popped.transition;
let preset = if spec.is_animated() {
spec.preset
} else {
PageTransition::IosPush
};
let (_driver, settle_spring) = make_driver(spec.timing);
let held = initial_progress.clamp(0.0, 1.0);
self.transition = Some(ActiveTransition {
driver: TransitionDriver::Held { value: held },
preset,
is_pop: true,
stashed: Some(popped),
settle_spring,
settled: false,
interactive: true,
restore_on_finalize: false,
hero_leaving: HashMap::new(),
hero_entering: HashMap::new(),
pending_spec: None,
});
self.publish_transition_start(from_depth, held, true, true);
if outgoing_focused {
self.needs_ime_clear = true;
}
}
fn settle_interactive(&mut self, complete: bool, progress_velocity: f64) {
let Some(t) = self.transition.as_mut() else {
return;
};
let from = t.driver.value();
let target = if complete { 1.0 } else { 0.0 };
t.driver = settle_driver(t.settle_spring, from, progress_velocity, target);
t.settled = false;
t.restore_on_finalize = !complete;
self.publish_transition_progress(from, Some(false));
}
pub(super) fn drive_edge_swipe(
&mut self,
ctx: &mut EventCtx<'_>,
event: &InputEvent,
t_ms: f64,
) -> EventResult {
let InputEvent::Pointer(p) = event else {
return EventResult::Ignored;
};
let width = ctx.size().width.max(1.0);
match p.phase {
PointerPhase::Move => {
self.edge.tracker.record(t_ms, p.position.x);
let dx = p.position.x - self.edge.down_start.x;
let progress = (dx / width).clamp(0.0, 1.0);
self.set_transition_progress(progress);
ctx.request_redraw();
EventResult::Handled
}
PointerPhase::Up => {
let finger_v = self.edge.tracker.velocity();
let progress = self
.transition
.as_ref()
.map(|t| t.driver.value())
.unwrap_or(0.0);
let complete =
progress > EDGE_SWIPE_COMMIT_PROGRESS || finger_v > EDGE_SWIPE_FLING_VELOCITY;
self.settle_interactive(complete, finger_v / width);
self.edge.active = false;
self.edge.armed = false;
self.edge.inner_claimed = false;
ctx.request_redraw();
EventResult::Handled
}
PointerPhase::Cancel => {
self.settle_interactive(false, 0.0);
self.edge.active = false;
self.edge.armed = false;
self.edge.inner_claimed = false;
ctx.request_redraw();
EventResult::Handled
}
PointerPhase::Down => EventResult::Handled,
}
}
pub(super) fn swipe_armable(&self) -> bool {
match self.pages.last() {
Some(top) => {
top.back == BackPolicy::Pop && top.pop_swipe.unwrap_or(self.pop_swipe_enabled)
}
None => false,
}
}
}