use std::collections::BTreeMap;
use std::fmt;
use serde::de::{self, DeserializeOwned, Deserializer, MapAccess, Visitor};
use serde::Deserialize;
use crate::value::WhiskerValue;
use crate::view::{set_event_listener, Element};
pub use crate::view::BindType;
pub fn bind_typed<E, F>(handle: Element, event_name: &'static str, bind_type: BindType, handler: F)
where
E: DeserializeOwned + Default + 'static,
F: Fn(E) + 'static,
{
set_event_listener(
handle,
event_name,
bind_type,
Box::new(move |value: WhiskerValue| {
let ev = value.deserialize_into::<E>().unwrap_or_else(|err| {
eprintln!(
"[whisker] event `{event_name}`: payload did not deserialize into `{}`: \
{err} (raw: {value:?}); calling handler with default",
std::any::type_name::<E>(),
);
E::default()
});
handler(ev);
}),
);
}
pub fn bind_unit<F>(handle: Element, event_name: &str, bind_type: BindType, handler: F)
where
F: Fn() + 'static,
{
set_event_listener(
handle,
event_name,
bind_type,
Box::new(move |_value: WhiskerValue| handler()),
);
}
#[derive(Debug, Clone, Default)]
pub struct Target {
pub id: String,
pub uid: i64,
pub dataset: BTreeMap<String, WhiskerValue>,
}
impl<'de> Deserialize<'de> for Target {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct TargetVisitor;
impl<'de> Visitor<'de> for TargetVisitor {
type Value = Target;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("an element sign (integer) or a target object")
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Target, E> {
Ok(Target {
uid: v,
..Default::default()
})
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Target, E> {
Ok(Target {
uid: v as i64,
..Default::default()
})
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<Target, E> {
Ok(Target {
uid: v as i64,
..Default::default()
})
}
fn visit_unit<E: de::Error>(self) -> Result<Target, E> {
Ok(Target::default())
}
fn visit_none<E: de::Error>(self) -> Result<Target, E> {
Ok(Target::default())
}
fn visit_map<A>(self, map: A) -> Result<Target, A::Error>
where
A: MapAccess<'de>,
{
#[derive(Deserialize)]
struct Obj {
#[serde(default)]
id: String,
#[serde(default)]
uid: i64,
#[serde(default)]
dataset: BTreeMap<String, WhiskerValue>,
}
let o = Obj::deserialize(de::value::MapAccessDeserializer::new(map))?;
Ok(Target {
id: o.id,
uid: o.uid,
dataset: o.dataset,
})
}
}
deserializer.deserialize_any(TargetVisitor)
}
}
#[derive(Debug, Clone, Copy, Default, Deserialize)]
pub struct Point {
#[serde(default)]
pub x: f64,
#[serde(default)]
pub y: f64,
}
#[derive(Debug, Clone, Copy, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Touch {
#[serde(default)]
pub identifier: i64,
#[serde(default)]
pub x: f64,
#[serde(default)]
pub y: f64,
#[serde(default)]
pub page_x: f64,
#[serde(default)]
pub page_y: f64,
#[serde(default)]
pub client_x: f64,
#[serde(default)]
pub client_y: f64,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct Event {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default)]
pub timestamp: f64,
#[serde(default)]
pub target: Target,
#[serde(rename = "currentTarget", default)]
pub current_target: Target,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TouchEvent {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default)]
pub timestamp: f64,
#[serde(default)]
pub target: Target,
#[serde(default)]
pub current_target: Target,
#[serde(default)]
pub detail: Point,
#[serde(default)]
pub touches: Vec<Touch>,
#[serde(default)]
pub changed_touches: Vec<Touch>,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct AnimationEvent {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default)]
pub timestamp: f64,
#[serde(default)]
pub target: Target,
#[serde(rename = "currentTarget", default)]
pub current_target: Target,
#[serde(rename = "animation_type", default)]
pub animation_type: String,
#[serde(rename = "animation_name", default)]
pub animation_name: String,
#[serde(rename = "new_animator", default)]
pub new_animator: bool,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct CustomEvent {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default)]
pub timestamp: f64,
#[serde(default)]
pub target: Target,
#[serde(rename = "currentTarget", default)]
pub current_target: Target,
#[serde(default)]
pub detail: WhiskerValue,
}
#[derive(Debug, Clone, Copy, Default, Deserialize)]
pub struct Size {
#[serde(default)]
pub width: f64,
#[serde(default)]
pub height: f64,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ScrollEvent {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default)]
pub timestamp: f64,
#[serde(default)]
pub target: Target,
#[serde(rename = "currentTarget", default)]
pub current_target: Target,
#[serde(default)]
pub detail: ScrollDetail,
}
#[derive(Debug, Clone, Copy, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ScrollDetail {
#[serde(default)]
pub scroll_left: f64,
#[serde(default)]
pub scroll_top: f64,
#[serde(default)]
pub scroll_width: f64,
#[serde(default)]
pub scroll_height: f64,
#[serde(default)]
pub delta_x: f64,
#[serde(default)]
pub delta_y: f64,
#[serde(default)]
pub is_dragging: bool,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct TextLayoutEvent {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default)]
pub timestamp: f64,
#[serde(default)]
pub target: Target,
#[serde(rename = "currentTarget", default)]
pub current_target: Target,
#[serde(default)]
pub detail: TextLayoutDetail,
}
#[derive(Debug, Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextLayoutDetail {
#[serde(default)]
pub line_count: i64,
#[serde(default)]
pub lines: Vec<TextLineInfo>,
#[serde(default)]
pub size: Size,
}
#[derive(Debug, Clone, Copy, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextLineInfo {
#[serde(default)]
pub start: i64,
#[serde(default)]
pub end: i64,
#[serde(default)]
pub ellipsis_count: i64,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SelectionChangeEvent {
#[serde(rename = "type", default)]
pub kind: String,
#[serde(default)]
pub timestamp: f64,
#[serde(default)]
pub target: Target,
#[serde(rename = "currentTarget", default)]
pub current_target: Target,
#[serde(default)]
pub detail: SelectionDetail,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct SelectionDetail {
#[serde(default)]
pub start: i64,
#[serde(default)]
pub end: i64,
#[serde(default)]
pub direction: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn touch_event_from_value_tree() {
let v = WhiskerValue::map([
("type", WhiskerValue::String("tap".into())),
("timestamp", WhiskerValue::Float(123.0)),
(
"detail",
WhiskerValue::map([
("x", WhiskerValue::Float(10.5)),
("y", WhiskerValue::Float(20.0)),
]),
),
(
"target",
WhiskerValue::map([
("id", WhiskerValue::String("btn".into())),
("uid", WhiskerValue::Int(7)),
]),
),
(
"touches",
WhiskerValue::Array(vec![WhiskerValue::map([
("identifier", WhiskerValue::Int(0)),
("pageX", WhiskerValue::Float(10.5)),
("pageY", WhiskerValue::Float(20.0)),
])]),
),
]);
let e: TouchEvent = v.deserialize_into().expect("deserialize TouchEvent");
assert_eq!(e.kind, "tap");
assert_eq!(e.detail.x, 10.5);
assert_eq!(e.target.id, "btn");
assert_eq!(e.target.uid, 7);
assert_eq!(e.touches.len(), 1);
assert_eq!(e.touches[0].page_x, 10.5);
}
#[test]
fn missing_fields_default_rather_than_fail() {
let e: TouchEvent = WhiskerValue::map([("type", WhiskerValue::String("touchend".into()))])
.deserialize_into()
.expect("partial body deserializes");
assert_eq!(e.kind, "touchend");
assert!(e.touches.is_empty());
assert_eq!(e.detail.x, 0.0);
}
#[test]
fn custom_event_keeps_opaque_detail() {
let v = WhiskerValue::map([(
"detail",
WhiskerValue::map([("scrollTop", WhiskerValue::Int(42))]),
)]);
let e: CustomEvent = v.deserialize_into().expect("deserialize CustomEvent");
match e.detail {
WhiskerValue::Map(m) => assert_eq!(m.get("scrollTop"), Some(&WhiskerValue::Int(42))),
other => panic!("expected Map detail, got {other:?}"),
}
}
#[test]
fn scroll_event_detail_camel_case_mapping() {
let v = WhiskerValue::map([
("type", WhiskerValue::String("scroll".into())),
(
"detail",
WhiskerValue::map([
("scrollLeft", WhiskerValue::Float(0.0)),
("scrollTop", WhiskerValue::Float(120.0)),
("scrollHeight", WhiskerValue::Float(2000.0)),
("scrollWidth", WhiskerValue::Float(375.0)),
("deltaY", WhiskerValue::Float(12.0)),
("isDragging", WhiskerValue::Bool(true)),
]),
),
]);
let e: ScrollEvent = v.deserialize_into().expect("deserialize ScrollEvent");
assert_eq!(e.kind, "scroll");
assert_eq!(e.detail.scroll_top, 120.0);
assert_eq!(e.detail.scroll_height, 2000.0);
assert_eq!(e.detail.delta_y, 12.0);
assert!(e.detail.is_dragging);
assert_eq!(e.detail.delta_x, 0.0);
}
#[test]
fn text_layout_event_nested_lines_and_size() {
let v = WhiskerValue::map([
("type", WhiskerValue::String("layout".into())),
(
"detail",
WhiskerValue::map([
("lineCount", WhiskerValue::Int(2)),
(
"size",
WhiskerValue::map([
("width", WhiskerValue::Float(300.0)),
("height", WhiskerValue::Float(40.0)),
]),
),
(
"lines",
WhiskerValue::Array(vec![
WhiskerValue::map([
("start", WhiskerValue::Int(0)),
("end", WhiskerValue::Int(10)),
("ellipsisCount", WhiskerValue::Int(0)),
]),
WhiskerValue::map([
("start", WhiskerValue::Int(10)),
("end", WhiskerValue::Int(18)),
("ellipsisCount", WhiskerValue::Int(3)),
]),
]),
),
]),
),
]);
let e: TextLayoutEvent = v.deserialize_into().expect("deserialize TextLayoutEvent");
assert_eq!(e.detail.line_count, 2);
assert_eq!(e.detail.size.width, 300.0);
assert_eq!(e.detail.lines.len(), 2);
assert_eq!(e.detail.lines[1].end, 18);
assert_eq!(e.detail.lines[1].ellipsis_count, 3);
}
#[test]
fn integer_target_signs_dont_blank_the_event() {
let v = WhiskerValue::map([
("type", WhiskerValue::String("scroll".into())),
("target", WhiskerValue::Int(33)),
("currentTarget", WhiskerValue::Int(33)),
(
"detail",
WhiskerValue::map([
("scrollLeft", WhiskerValue::Float(640.0)),
("scrollWidth", WhiskerValue::Float(832.0)),
("isDragging", WhiskerValue::Bool(true)),
]),
),
]);
let e: ScrollEvent = v
.deserialize_into()
.expect("deserialize with integer target");
assert_eq!(e.target.uid, 33); assert_eq!(e.target.id, ""); assert_eq!(e.current_target.uid, 33);
assert_eq!(e.detail.scroll_left, 640.0);
assert_eq!(e.detail.scroll_width, 832.0);
assert!(e.detail.is_dragging);
}
#[test]
fn touch_event_integer_target() {
let v = WhiskerValue::map([
("type", WhiskerValue::String("tap".into())),
("target", WhiskerValue::Int(7)),
(
"detail",
WhiskerValue::map([
("x", WhiskerValue::Float(10.0)),
("y", WhiskerValue::Float(20.0)),
]),
),
]);
let e: TouchEvent = v
.deserialize_into()
.expect("deserialize TouchEvent int target");
assert_eq!(e.target.uid, 7);
assert_eq!(e.detail.x, 10.0);
}
}