use crate::modifier::{Modifier, PointerEventKind};
use cranpose_core::{ownedMutableStateOf, OwnedMutableState};
use cranpose_foundation::nodes::input::gestures::{TransformGesture, TransformGestureEvent};
use cranpose_foundation::DRAG_THRESHOLD;
use cranpose_ui_graphics::{GraphicsLayer, Point, TransformOrigin};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
const SCALE_EPSILON: f32 = 1e-3;
#[derive(Clone)]
pub struct ZoomState {
inner: Rc<ZoomStateInner>,
}
struct ZoomStateInner {
scale: OwnedMutableState<f32>,
offset_x: OwnedMutableState<f32>,
offset_y: OwnedMutableState<f32>,
min_scale: Cell<f32>,
max_scale: Cell<f32>,
}
impl Default for ZoomState {
fn default() -> Self {
Self::new()
}
}
impl ZoomState {
pub fn new() -> Self {
Self::with_scale_range(1.0, 8.0)
}
pub fn with_scale_range(min_scale: f32, max_scale: f32) -> Self {
assert!(
min_scale > 0.0 && max_scale >= min_scale,
"invalid zoom range {min_scale}..{max_scale}"
);
Self {
inner: Rc::new(ZoomStateInner {
scale: ownedMutableStateOf(1.0f32.clamp(min_scale, max_scale)),
offset_x: ownedMutableStateOf(0.0f32),
offset_y: ownedMutableStateOf(0.0f32),
min_scale: Cell::new(min_scale),
max_scale: Cell::new(max_scale),
}),
}
}
pub fn id(&self) -> u64 {
Rc::as_ptr(&self.inner) as usize as u64
}
pub fn scale(&self) -> f32 {
self.inner.scale.with(|s| *s)
}
pub fn scale_non_reactive(&self) -> f32 {
self.inner.scale.get_non_reactive()
}
pub fn offset(&self) -> Point {
Point {
x: self.inner.offset_x.with(|v| *v),
y: self.inner.offset_y.with(|v| *v),
}
}
pub fn offset_non_reactive(&self) -> Point {
Point {
x: self.inner.offset_x.get_non_reactive(),
y: self.inner.offset_y.get_non_reactive(),
}
}
pub fn min_scale(&self) -> f32 {
self.inner.min_scale.get()
}
pub fn max_scale(&self) -> f32 {
self.inner.max_scale.get()
}
pub fn is_transformed(&self) -> bool {
let offset = self.offset_non_reactive();
(self.scale_non_reactive() - 1.0).abs() > SCALE_EPSILON
|| offset.x != 0.0
|| offset.y != 0.0
}
pub fn set_scale(&self, scale: f32) {
let clamped = scale.clamp(self.min_scale(), self.max_scale());
self.inner.scale.set(clamped);
}
pub fn set_offset(&self, offset: Point) {
self.inner.offset_x.set(offset.x);
self.inner.offset_y.set(offset.y);
}
pub fn reset(&self) {
self.set_scale(1.0);
self.set_offset(Point { x: 0.0, y: 0.0 });
}
pub fn apply_transform(&self, centroid: Point, pan: Point, zoom: f32) {
let old_scale = self.scale_non_reactive();
let new_scale = (old_scale * zoom).clamp(self.min_scale(), self.max_scale());
let effective_zoom = new_scale / old_scale;
let old_offset = self.offset_non_reactive();
let new_offset = Point {
x: centroid.x - (centroid.x - old_offset.x) * effective_zoom + pan.x,
y: centroid.y - (centroid.y - old_offset.y) * effective_zoom + pan.y,
};
if new_scale != old_scale {
self.inner.scale.set(new_scale);
}
if new_offset != old_offset {
self.set_offset(new_offset);
}
}
pub fn layer(&self) -> GraphicsLayer {
let scale = self.scale();
let offset = self.offset();
GraphicsLayer {
scale_x: scale,
scale_y: scale,
translation_x: offset.x,
translation_y: offset.y,
transform_origin: TransformOrigin::new(0.0, 0.0),
..Default::default()
}
}
}
#[derive(Default)]
struct ZoomGestureState {
tracker: TransformGesture,
active: bool,
travel: f32,
}
impl Modifier {
pub fn zoomable(self, state: ZoomState) -> Self {
let gesture_state = Rc::new(RefCell::new(ZoomGestureState::default()));
let key = state.id();
self.pointer_input(key, move |scope| {
let state = state.clone();
let gesture_state = gesture_state.clone();
async move {
scope
.await_pointer_event_scope(|await_scope| async move {
loop {
let event = await_scope.await_pointer_event().await;
match event.kind {
PointerEventKind::Zoom => {
if !event.is_consumed() && event.zoom_delta != 1.0 {
state.apply_transform(
event.global_position,
Point { x: 0.0, y: 0.0 },
event.zoom_delta,
);
event.consume();
}
}
PointerEventKind::Cancel => {
let mut gs = gesture_state.borrow_mut();
gs.tracker.reset();
gs.active = false;
gs.travel = 0.0;
}
PointerEventKind::Down
| PointerEventKind::Move
| PointerEventKind::Up => {
let mut gs = gesture_state.borrow_mut();
if event.is_consumed() {
gs.tracker.reset();
gs.active = false;
gs.travel = 0.0;
continue;
}
let tracked =
event.copy_with_local_position(event.global_position);
let step = gs.tracker.handle_event(&tracked);
if event.kind == PointerEventKind::Down {
if gs.tracker.pointer_count() >= 2 {
gs.active = true;
} else {
gs.travel = 0.0;
}
if event.id != 0 {
event.consume();
}
continue;
}
match step {
TransformGestureEvent::Transform {
pan,
zoom,
centroid,
pointer_count,
} => {
if pointer_count >= 2 {
gs.active = true;
} else if !gs.active && state.is_transformed() {
gs.travel += (pan.x * pan.x + pan.y * pan.y).sqrt();
if gs.travel > DRAG_THRESHOLD {
gs.active = true;
}
}
if gs.active {
state.apply_transform(centroid, pan, zoom);
event.consume();
}
}
TransformGestureEvent::Ended => {
let was_active = gs.active;
gs.active = false;
gs.travel = 0.0;
if was_active {
event.consume();
}
}
TransformGestureEvent::None => {
if event.id != 0
|| (gs.active
&& matches!(
event.kind,
PointerEventKind::Up
| PointerEventKind::Cancel
))
{
event.consume();
}
}
}
}
PointerEventKind::Scroll
| PointerEventKind::Enter
| PointerEventKind::Exit => {}
}
}
})
.await;
}
})
}
}
#[cfg(test)]
#[path = "tests/zoom_tests.rs"]
mod tests;