1use std::collections::BTreeMap;
28use std::fmt;
29
30use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
31use serde::Deserialize;
32
33use crate::value::WhiskerValue;
34use crate::view::{set_event_listener, Element};
35
36pub use crate::view::BindType;
40
41pub fn bind_typed<E, F>(handle: Element, event_name: &'static str, bind_type: BindType, handler: F)
57where
58 E: DeserializeOwned + Default + 'static,
59 F: Fn(E) + 'static,
60{
61 set_event_listener(
62 handle,
63 event_name,
64 bind_type,
65 Box::new(move |value: WhiskerValue| {
66 let ev = value.deserialize_into::<E>().unwrap_or_else(|err| {
67 eprintln!(
68 "[whisker] event `{event_name}`: payload did not deserialize into `{}`: \
69 {err} (raw: {value:?}); calling handler with default",
70 std::any::type_name::<E>(),
71 );
72 E::default()
73 });
74 handler(ev);
75 }),
76 );
77}
78
79pub fn bind_unit<F>(handle: Element, event_name: &str, bind_type: BindType, handler: F)
84where
85 F: Fn() + 'static,
86{
87 set_event_listener(
88 handle,
89 event_name,
90 bind_type,
91 Box::new(move |_value: WhiskerValue| handler()),
92 );
93}
94
95#[derive(Debug, Clone, Default)]
99#[non_exhaustive]
100pub struct Target {
101 pub id: String,
103 pub uid: i64,
105 pub dataset: BTreeMap<String, WhiskerValue>,
108}
109
110impl<'de> Deserialize<'de> for Target {
120 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
121 where
122 D: Deserializer<'de>,
123 {
124 struct TargetVisitor;
125 impl<'de> Visitor<'de> for TargetVisitor {
126 type Value = Target;
127 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
128 f.write_str("an element sign (integer) or a target object")
129 }
130 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Target, E> {
131 Ok(Target {
132 uid: v,
133 ..Default::default()
134 })
135 }
136 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Target, E> {
137 Ok(Target {
138 uid: v as i64,
139 ..Default::default()
140 })
141 }
142 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Target, E> {
143 Ok(Target {
144 uid: v as i64,
145 ..Default::default()
146 })
147 }
148 fn visit_unit<E: de::Error>(self) -> Result<Target, E> {
149 Ok(Target::default())
150 }
151 fn visit_none<E: de::Error>(self) -> Result<Target, E> {
152 Ok(Target::default())
153 }
154 fn visit_map<A>(self, map: A) -> Result<Target, A::Error>
155 where
156 A: MapAccess<'de>,
157 {
158 #[derive(Deserialize)]
159 struct Obj {
160 #[serde(default)]
161 id: String,
162 #[serde(default)]
163 uid: i64,
164 #[serde(default)]
165 dataset: BTreeMap<String, WhiskerValue>,
166 }
167 let o = Obj::deserialize(de::value::MapAccessDeserializer::new(map))?;
168 Ok(Target {
169 id: o.id,
170 uid: o.uid,
171 dataset: o.dataset,
172 })
173 }
174 }
175 deserializer.deserialize_any(TargetVisitor)
176 }
177}
178
179#[derive(Debug, Clone, Copy, Default, Deserialize)]
182#[non_exhaustive]
183pub struct Point {
184 #[serde(default)]
185 pub x: f64,
186 #[serde(default)]
187 pub y: f64,
188}
189
190#[derive(Debug, Clone, Copy, Default, Deserialize)]
192#[serde(rename_all = "camelCase")]
193#[non_exhaustive]
194pub struct Touch {
195 #[serde(default)]
197 pub identifier: i64,
198 #[serde(default)]
200 pub x: f64,
201 #[serde(default)]
202 pub y: f64,
203 #[serde(default)]
205 pub page_x: f64,
206 #[serde(default)]
207 pub page_y: f64,
208 #[serde(default)]
210 pub client_x: f64,
211 #[serde(default)]
212 pub client_y: f64,
213}
214
215#[derive(Debug, Clone, Default, Deserialize)]
217#[non_exhaustive]
218pub struct Event {
219 #[serde(rename = "type", default)]
221 pub kind: String,
222 #[serde(default)]
224 pub timestamp: f64,
225 #[serde(default)]
227 pub target: Target,
228 #[serde(rename = "currentTarget", default)]
230 pub current_target: Target,
231}
232
233#[derive(Debug, Clone, Default, Deserialize)]
237#[serde(rename_all = "camelCase")]
238#[non_exhaustive]
239pub struct TouchEvent {
240 #[serde(rename = "type", default)]
241 pub kind: String,
242 #[serde(default)]
243 pub timestamp: f64,
244 #[serde(default)]
245 pub target: Target,
246 #[serde(default)]
247 pub current_target: Target,
248 #[serde(default)]
250 pub detail: Point,
251 #[serde(default)]
253 pub touches: Vec<Touch>,
254 #[serde(default)]
256 pub changed_touches: Vec<Touch>,
257}
258
259#[derive(Debug, Clone, Default, Deserialize)]
261#[non_exhaustive]
262pub struct AnimationEvent {
263 #[serde(rename = "type", default)]
264 pub kind: String,
265 #[serde(default)]
266 pub timestamp: f64,
267 #[serde(default)]
268 pub target: Target,
269 #[serde(rename = "currentTarget", default)]
270 pub current_target: Target,
271 #[serde(rename = "animation_type", default)]
273 pub animation_type: String,
274 #[serde(rename = "animation_name", default)]
276 pub animation_name: String,
277 #[serde(rename = "new_animator", default)]
278 pub new_animator: bool,
279}
280
281#[derive(Debug, Clone, Default, Deserialize)]
285#[non_exhaustive]
286pub struct CustomEvent {
287 #[serde(rename = "type", default)]
288 pub kind: String,
289 #[serde(default)]
290 pub timestamp: f64,
291 #[serde(default)]
292 pub target: Target,
293 #[serde(rename = "currentTarget", default)]
294 pub current_target: Target,
295 #[serde(default)]
297 pub detail: WhiskerValue,
298}
299
300#[derive(Debug, Clone, Copy, Default, Deserialize)]
302#[non_exhaustive]
303pub struct Size {
304 #[serde(default)]
305 pub width: f64,
306 #[serde(default)]
307 pub height: f64,
308}
309
310#[derive(Debug, Clone, Default, Deserialize)]
318#[non_exhaustive]
319pub struct ScrollEvent {
320 #[serde(rename = "type", default)]
321 pub kind: String,
322 #[serde(default)]
323 pub timestamp: f64,
324 #[serde(default)]
325 pub target: Target,
326 #[serde(rename = "currentTarget", default)]
327 pub current_target: Target,
328 #[serde(default)]
329 pub detail: ScrollDetail,
330}
331
332#[derive(Debug, Clone, Copy, Default, Deserialize)]
335#[serde(rename_all = "camelCase")]
336#[non_exhaustive]
337pub struct ScrollDetail {
338 #[serde(default)]
340 pub scroll_left: f64,
341 #[serde(default)]
343 pub scroll_top: f64,
344 #[serde(default)]
346 pub scroll_width: f64,
347 #[serde(default)]
349 pub scroll_height: f64,
350 #[serde(default)]
352 pub delta_x: f64,
353 #[serde(default)]
355 pub delta_y: f64,
356 #[serde(default)]
358 pub is_dragging: bool,
359}
360
361#[derive(Debug, Clone, Default, Deserialize)]
365#[non_exhaustive]
366pub struct TextLayoutEvent {
367 #[serde(rename = "type", default)]
368 pub kind: String,
369 #[serde(default)]
370 pub timestamp: f64,
371 #[serde(default)]
372 pub target: Target,
373 #[serde(rename = "currentTarget", default)]
374 pub current_target: Target,
375 #[serde(default)]
376 pub detail: TextLayoutDetail,
377}
378
379#[derive(Debug, Clone, Default, Deserialize)]
381#[serde(rename_all = "camelCase")]
382#[non_exhaustive]
383pub struct TextLayoutDetail {
384 #[serde(default)]
386 pub line_count: i64,
387 #[serde(default)]
389 pub lines: Vec<TextLineInfo>,
390 #[serde(default)]
392 pub size: Size,
393}
394
395#[derive(Debug, Clone, Copy, Default, Deserialize)]
397#[serde(rename_all = "camelCase")]
398#[non_exhaustive]
399pub struct TextLineInfo {
400 #[serde(default)]
402 pub start: i64,
403 #[serde(default)]
405 pub end: i64,
406 #[serde(default)]
409 pub ellipsis_count: i64,
410}
411
412#[derive(Debug, Clone, Default, Deserialize)]
414#[non_exhaustive]
415pub struct SelectionChangeEvent {
416 #[serde(rename = "type", default)]
417 pub kind: String,
418 #[serde(default)]
419 pub timestamp: f64,
420 #[serde(default)]
421 pub target: Target,
422 #[serde(rename = "currentTarget", default)]
423 pub current_target: Target,
424 #[serde(default)]
425 pub detail: SelectionDetail,
426}
427
428#[derive(Debug, Clone, Default, Deserialize)]
430#[non_exhaustive]
431pub struct SelectionDetail {
432 #[serde(default)]
434 pub start: i64,
435 #[serde(default)]
437 pub end: i64,
438 #[serde(default)]
440 pub direction: String,
441}
442
443#[cfg(test)]
444mod tests {
445 use super::*;
446
447 #[test]
448 fn touch_event_from_value_tree() {
449 let v = WhiskerValue::map([
451 ("type", WhiskerValue::String("tap".into())),
452 ("timestamp", WhiskerValue::Float(123.0)),
453 (
454 "detail",
455 WhiskerValue::map([
456 ("x", WhiskerValue::Float(10.5)),
457 ("y", WhiskerValue::Float(20.0)),
458 ]),
459 ),
460 (
461 "target",
462 WhiskerValue::map([
463 ("id", WhiskerValue::String("btn".into())),
464 ("uid", WhiskerValue::Int(7)),
465 ]),
466 ),
467 (
468 "touches",
469 WhiskerValue::Array(vec![WhiskerValue::map([
470 ("identifier", WhiskerValue::Int(0)),
471 ("pageX", WhiskerValue::Float(10.5)),
472 ("pageY", WhiskerValue::Float(20.0)),
473 ])]),
474 ),
475 ]);
476
477 let e: TouchEvent = v.deserialize_into().expect("deserialize TouchEvent");
478 assert_eq!(e.kind, "tap");
479 assert_eq!(e.detail.x, 10.5);
480 assert_eq!(e.target.id, "btn");
481 assert_eq!(e.target.uid, 7);
482 assert_eq!(e.touches.len(), 1);
483 assert_eq!(e.touches[0].page_x, 10.5);
484 }
485
486 #[test]
487 fn missing_fields_default_rather_than_fail() {
488 let e: TouchEvent = WhiskerValue::map([("type", WhiskerValue::String("touchend".into()))])
491 .deserialize_into()
492 .expect("partial body deserializes");
493 assert_eq!(e.kind, "touchend");
494 assert!(e.touches.is_empty());
495 assert_eq!(e.detail.x, 0.0);
496 }
497
498 #[test]
499 fn custom_event_keeps_opaque_detail() {
500 let v = WhiskerValue::map([(
501 "detail",
502 WhiskerValue::map([("scrollTop", WhiskerValue::Int(42))]),
503 )]);
504 let e: CustomEvent = v.deserialize_into().expect("deserialize CustomEvent");
505 match e.detail {
506 WhiskerValue::Map(m) => assert_eq!(m.get("scrollTop"), Some(&WhiskerValue::Int(42))),
507 other => panic!("expected Map detail, got {other:?}"),
508 }
509 }
510
511 #[test]
512 fn scroll_event_detail_camel_case_mapping() {
513 let v = WhiskerValue::map([
515 ("type", WhiskerValue::String("scroll".into())),
516 (
517 "detail",
518 WhiskerValue::map([
519 ("scrollLeft", WhiskerValue::Float(0.0)),
520 ("scrollTop", WhiskerValue::Float(120.0)),
521 ("scrollHeight", WhiskerValue::Float(2000.0)),
522 ("scrollWidth", WhiskerValue::Float(375.0)),
523 ("deltaY", WhiskerValue::Float(12.0)),
524 ("isDragging", WhiskerValue::Bool(true)),
525 ]),
526 ),
527 ]);
528 let e: ScrollEvent = v.deserialize_into().expect("deserialize ScrollEvent");
529 assert_eq!(e.kind, "scroll");
530 assert_eq!(e.detail.scroll_top, 120.0);
531 assert_eq!(e.detail.scroll_height, 2000.0);
532 assert_eq!(e.detail.delta_y, 12.0);
533 assert!(e.detail.is_dragging);
534 assert_eq!(e.detail.delta_x, 0.0);
536 }
537
538 #[test]
539 fn text_layout_event_nested_lines_and_size() {
540 let v = WhiskerValue::map([
541 ("type", WhiskerValue::String("layout".into())),
542 (
543 "detail",
544 WhiskerValue::map([
545 ("lineCount", WhiskerValue::Int(2)),
546 (
547 "size",
548 WhiskerValue::map([
549 ("width", WhiskerValue::Float(300.0)),
550 ("height", WhiskerValue::Float(40.0)),
551 ]),
552 ),
553 (
554 "lines",
555 WhiskerValue::Array(vec![
556 WhiskerValue::map([
557 ("start", WhiskerValue::Int(0)),
558 ("end", WhiskerValue::Int(10)),
559 ("ellipsisCount", WhiskerValue::Int(0)),
560 ]),
561 WhiskerValue::map([
562 ("start", WhiskerValue::Int(10)),
563 ("end", WhiskerValue::Int(18)),
564 ("ellipsisCount", WhiskerValue::Int(3)),
565 ]),
566 ]),
567 ),
568 ]),
569 ),
570 ]);
571 let e: TextLayoutEvent = v.deserialize_into().expect("deserialize TextLayoutEvent");
572 assert_eq!(e.detail.line_count, 2);
573 assert_eq!(e.detail.size.width, 300.0);
574 assert_eq!(e.detail.lines.len(), 2);
575 assert_eq!(e.detail.lines[1].end, 18);
576 assert_eq!(e.detail.lines[1].ellipsis_count, 3);
577 }
578
579 #[test]
580 fn integer_target_signs_dont_blank_the_event() {
581 let v = WhiskerValue::map([
587 ("type", WhiskerValue::String("scroll".into())),
588 ("target", WhiskerValue::Int(33)),
589 ("currentTarget", WhiskerValue::Int(33)),
590 (
591 "detail",
592 WhiskerValue::map([
593 ("scrollLeft", WhiskerValue::Float(640.0)),
594 ("scrollWidth", WhiskerValue::Float(832.0)),
595 ("isDragging", WhiskerValue::Bool(true)),
596 ]),
597 ),
598 ]);
599 let e: ScrollEvent = v
600 .deserialize_into()
601 .expect("deserialize with integer target");
602 assert_eq!(e.target.uid, 33); assert_eq!(e.target.id, ""); assert_eq!(e.current_target.uid, 33);
605 assert_eq!(e.detail.scroll_left, 640.0);
607 assert_eq!(e.detail.scroll_width, 832.0);
608 assert!(e.detail.is_dragging);
609 }
610
611 #[test]
612 fn touch_event_integer_target() {
613 let v = WhiskerValue::map([
614 ("type", WhiskerValue::String("tap".into())),
615 ("target", WhiskerValue::Int(7)),
616 (
617 "detail",
618 WhiskerValue::map([
619 ("x", WhiskerValue::Float(10.0)),
620 ("y", WhiskerValue::Float(20.0)),
621 ]),
622 ),
623 ]);
624 let e: TouchEvent = v
625 .deserialize_into()
626 .expect("deserialize TouchEvent int target");
627 assert_eq!(e.target.uid, 7);
628 assert_eq!(e.detail.x, 10.0);
629 }
630}