⎇ 30467600bcd9effa15e8338fb32d357c1edab660
00_unanchay/pluma/pluma-deck-core/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
1//! Deck core — máquinas de estado agnósticas para presentar, en dos modos:
2//!
3//! - **Lineal** ([`DeckState`], este archivo): strip horizontal de páginas con
4//! drag/snap. Dados los eventos crudos de pointer (coords + viewport width
5//! + n_pages) decide cuándo arrancar drag horizontal, cuánto trasladar el
6//! strip y a qué página snapear al soltar.
7//! - **Espacial** ([`recorrido`], tipo Prezi): lienzo 2D infinito con marcos en
8//! coordenadas de mundo y una ruta de cámara ([`camara::Camara`]) que vuela
9//! entre ellos con zoom/pan/giro. El modo lineal es su caso degenerado.
10//!
11//! Todo agnóstico: sin DOM, sin wasm-bindgen, sin render.
12
13pub mod camara;
14pub mod recorrido;
15/// Adaptador opcional pluma → Recorrido. Sólo con la feature `pluma`.
16#[cfg(feature = "pluma")]
17pub mod adaptador;
18
19pub use camara::{Camara, Ease, Rect, FIT_MARGEN, ZOOM_MAX, ZOOM_MIN};
20pub use recorrido::{
21 Autoplay, ContenidoMarco, Marco, MarcoId, Recorrido, RecorridoState, RejillaOpts,
22 DURACION_PASO_S, DWELL_S,
23};
24
25/// Umbral en pixels para confirmar gesto horizontal vs vertical.
26pub const DRAG_DECISION_PX: f64 = 8.0;
27/// Cuán más horizontal que vertical debe ser el delta para considerarse "swipe".
28pub const HORIZONTAL_BIAS: f64 = 1.3;
29
30#[derive(Clone, Debug, Default)]
31pub struct DeckState {
32 pub current_index: usize,
33 pointer_start: Option<(f64, f64, i32)>,
34 drag_active: bool,
35 drag_start_offset: f64,
36}
37
38#[derive(Clone, Debug, PartialEq)]
39pub enum DragOutcome {
40 /// Aún no hay decisión — esperar más movimiento.
41 Idle,
42 /// Empezar drag horizontal: el host debe capturar el pointer.
43 StartHorizontal { pointer_id: i32 },
44 /// Movimiento vertical predominante — host debe ceder al scroll nativo.
45 CancelVertical,
46 /// Drag en curso — host debe trasladar el strip a este offset.
47 DragOffset(f64),
48}
49
50#[derive(Clone, Debug, PartialEq)]
51pub struct SnapResult {
52 pub target_index: usize,
53 pub offset_px: f64,
54 pub changed: bool,
55}
56
57impl DeckState {
58 pub fn new() -> Self { Self::default() }
59
60 /// Marca el inicio de un gesto. `viewport_width` se usa para anclar el
61 /// drag_start_offset a la página visible actual.
62 pub fn pointer_down(&mut self, x: f64, y: f64, pointer_id: i32, viewport_width: f64) {
63 self.pointer_start = Some((x, y, pointer_id));
64 self.drag_active = false;
65 self.drag_start_offset = -(self.current_index as f64) * viewport_width;
66 }
67
68 /// Procesa un movimiento. Devuelve la acción que el host debe ejecutar.
69 pub fn pointer_move(&mut self, x: f64, y: f64) -> DragOutcome {
70 let Some((sx, sy, pid)) = self.pointer_start else {
71 return DragOutcome::Idle;
72 };
73 let dx = x - sx;
74 let dy = y - sy;
75 if !self.drag_active {
76 let abs_dx = dx.abs();
77 let abs_dy = dy.abs();
78 if abs_dx > DRAG_DECISION_PX && abs_dx > abs_dy * HORIZONTAL_BIAS {
79 self.drag_active = true;
80 return DragOutcome::StartHorizontal { pointer_id: pid };
81 } else if abs_dy > DRAG_DECISION_PX {
82 self.pointer_start = None;
83 return DragOutcome::CancelVertical;
84 } else {
85 return DragOutcome::Idle;
86 }
87 }
88 DragOutcome::DragOffset(self.drag_start_offset + dx)
89 }
90
91 /// Finaliza el gesto. Si había drag activo, calcula la página snap y
92 /// actualiza `current_index`. `current_offset` viene del estado real
93 /// del strip (el host lee CSS transform / variable).
94 pub fn pointer_end(
95 &mut self,
96 current_offset: f64,
97 viewport_width: f64,
98 n_pages: usize,
99 ) -> Option<SnapResult> {
100 let was_active = self.drag_active;
101 self.drag_active = false;
102 self.pointer_start = None;
103 if !was_active || viewport_width <= 0.0 || n_pages == 0 {
104 return None;
105 }
106 let raw = -current_offset / viewport_width;
107 let target = (raw.round().max(0.0) as usize).min(n_pages - 1);
108 let offset_px = -(target as f64) * viewport_width;
109 let changed = self.current_index != target;
110 self.current_index = target;
111 Some(SnapResult { target_index: target, offset_px, changed })
112 }
113
114 /// Salto programático (click en tabs externos). Devuelve el offset
115 /// resultante para que el host lo aplique al strip.
116 pub fn goto(&mut self, index: usize, viewport_width: f64) -> SnapResult {
117 let offset_px = -(index as f64) * viewport_width;
118 let changed = self.current_index != index;
119 self.current_index = index;
120 SnapResult { target_index: index, offset_px, changed }
121 }
122
123 /// Reposiciona tras un resize. Devuelve el offset que el host debe
124 /// aplicar sin animación.
125 pub fn reposition(&self, viewport_width: f64) -> f64 {
126 -(self.current_index as f64) * viewport_width
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn vertical_drag_is_cancelled() {
136 let mut s = DeckState::new();
137 s.pointer_down(100.0, 100.0, 1, 800.0);
138 // Movimiento vertical mayor que el umbral.
139 let r = s.pointer_move(100.0, 120.0);
140 assert_eq!(r, DragOutcome::CancelVertical);
141 }
142
143 #[test]
144 fn horizontal_drag_starts_after_threshold() {
145 let mut s = DeckState::new();
146 s.pointer_down(100.0, 100.0, 7, 800.0);
147 // Justo por debajo del umbral → Idle.
148 assert_eq!(s.pointer_move(105.0, 100.0), DragOutcome::Idle);
149 // Sobre el umbral con bias horizontal → Start.
150 let r = s.pointer_move(120.0, 100.0);
151 assert_eq!(r, DragOutcome::StartHorizontal { pointer_id: 7 });
152 }
153
154 #[test]
155 fn snap_rounds_to_nearest_page() {
156 let mut s = DeckState::new();
157 s.current_index = 1;
158 s.pointer_down(0.0, 0.0, 1, 1000.0); // drag_start_offset = -1000
159 // Forzar drag activo
160 s.pointer_move(20.0, 0.0);
161 // Offset actual = -1000 + 20 = -980 → target round(980/1000) = 1, sin cambio
162 let r = s.pointer_end(-980.0, 1000.0, 3).unwrap();
163 assert_eq!(r.target_index, 1);
164 assert!(!r.changed);
165 // Mover lo suficiente para snapear a página 0
166 s.pointer_down(0.0, 0.0, 1, 1000.0);
167 s.pointer_move(600.0, 0.0);
168 let r = s.pointer_end(-400.0, 1000.0, 3).unwrap();
169 assert_eq!(r.target_index, 0);
170 assert!(r.changed);
171 }
172
173 #[test]
174 fn snap_clamps_to_bounds() {
175 let mut s = DeckState::new();
176 s.current_index = 2;
177 s.pointer_down(0.0, 0.0, 1, 500.0);
178 s.pointer_move(50.0, 0.0);
179 // Offset muy a la izquierda → debería clamp a n_pages-1
180 let r = s.pointer_end(-9999.0, 500.0, 3).unwrap();
181 assert_eq!(r.target_index, 2);
182 }
183
184 #[test]
185 fn goto_updates_index_and_offset() {
186 let mut s = DeckState::new();
187 let r = s.goto(2, 800.0);
188 assert_eq!(r.target_index, 2);
189 assert_eq!(r.offset_px, -1600.0);
190 assert!(r.changed);
191 // segundo goto al mismo índice → changed=false
192 let r = s.goto(2, 800.0);
193 assert!(!r.changed);
194 }
195}