Skip to main content

pathland_core/
events.rs

1//! Typed raw-input events (host → guest).
2//!
3//! Events are the **raw inputs** the renderer reports: pointer down/move/up and
4//! key down/up, each carrying a host-resolved `targetId`. High-level
5//! interpretations (tap, drag, long-press) are built by the guest/app from
6//! these raw inputs and are NOT part of the protocol.
7//!
8//! Encode a raw input with `Event::encode`, decode a ring opcode with
9//! `Event::try_from`.
10
11use alloc::string::String;
12use alloc::string::ToString;
13use crate::category;
14use crate::flag;
15use crate::Opcode;
16
17/// A raw-input event (host → guest).
18///
19/// `Event` is **not** `Copy`: [`Event::TextChanged`] owns the new field text
20/// and [`Event::Navigate`] owns an optional URL. Simple events round-trip
21/// through `encode`/`try_from` as pure 16-byte opcodes; `TextChanged` and
22/// `Navigate` with a URL are encoded/decoded at the batch level (their strings
23/// live in the string section / event arena, referenced by a relative/absolute
24/// offset).
25#[derive(Debug, Clone, PartialEq)]
26pub enum Event {
27    /// Pointer button pressed at a viewport-relative point.
28    PointerDown {
29        target: u32,
30        x: f32,
31        y: f32,
32        secondary: bool,
33    },
34    /// Pointer moved; optionally entered/left the node (hover).
35    PointerMove {
36        target: u32,
37        x: f32,
38        y: f32,
39        hovering: bool,
40        leaving: bool,
41    },
42    /// Pointer button released at a viewport-relative point.
43    PointerUp {
44        target: u32,
45        x: f32,
46        y: f32,
47        secondary: bool,
48    },
49    /// Key pressed (possibly an auto-repeat).
50    KeyDown {
51        target: u32,
52        key_code: u16,
53        modifiers: u8,
54        repeat: bool,
55    },
56    /// Key released.
57    KeyUp {
58        target: u32,
59        key_code: u16,
60        modifiers: u8,
61    },
62    /// A control's value changed (host → guest). The renderer resolves the
63    /// semantic value (e.g. a slider thumb dragged along its track) and reports
64    /// it here; the guest/app writes it into its bound state.
65    ValueChanged {
66        target: u32,
67        value: f32,
68    },
69    /// A text field's value changed (host → guest). The new text is resolved
70    /// from the batch's string section (see the doc comment on this enum).
71    TextChanged {
72        target: u32,
73        value: String,
74    },
75    /// A node gained/lost focus (host → guest). Draft command 0x08.
76    FocusChanged { target: u32, focused: bool },
77    /// An editing session began/ended (host → guest). Draft command 0x09.
78    EditingChanged { target: u32, editing: bool },
79    /// The user committed a field (host → guest). Draft command 0x0A.
80    Submit { target: u32 },
81    /// A scroll container's offset changed (host → guest). Draft command 0x0B.
82    Scroll {
83        target: u32,
84        offset_x: f32,
85        offset_y: f32,
86    },
87    /// Raw wheel/trackpad deltas (host → guest). Draft command 0x0C.
88    Wheel {
89        target: u32,
90        delta_x: f32,
91        delta_y: f32,
92    },
93    /// A `DATE_PICKER`'s value changed (host → guest). Draft command 0x0D;
94    /// `days` is I32 (pre-1970 negative), `millis` is millis of day.
95    DateChanged {
96        target: u32,
97        days: i32,
98        millis: u32,
99    },
100    /// A global navigation request (host → guest). Draft command 0x0E; never
101    /// node-keyed. `url: Some(...)` carries the destination URL the platform
102    /// navigated to (browser `popstate`/back/forward/deep-link), resolved from
103    /// the string section / event arena; `url: None` is a native back request.
104    Navigate { url: Option<String> },
105    /// A bound `MEDIA_CONTROLLER`'s playback state changed (host → guest).
106    /// Draft command 0x0F; reported only when `PLAYBACK_STATE` is bound.
107    MediaPlayStateChanged { target: u32, playing: bool },
108    /// A bound controller's current playback position (host → guest). Draft
109    /// command 0x10; `seconds` is the position in seconds (f32), reported while
110    /// playing. An app-initiated seek (`MEDIA_POSITION` change) is not echoed.
111    MediaTimeUpdated { target: u32, seconds: f32 },
112    /// A bound controller reached the end of its media (host → guest). Draft
113    /// command 0x11.
114    MediaEnded { target: u32 },
115    /// A bound controller's volume changed (host → guest). Draft command 0x12;
116    /// `volume` is in the range 0..1 (f32).
117    MediaVolumeChanged { target: u32, volume: f32 },
118    /// A bound controller's media duration became known/changed (host → guest).
119    /// Draft command 0x14; `seconds` is the duration (f32). Reported when the
120    /// resource loads and only when the value changes.
121    MediaDurationChanged { target: u32, seconds: f32 },
122    /// A `SIZE_THAT_FITS` slot's selected candidate changed (host → guest).
123    /// Draft command 0x13; `index` is the derived fit index (f32), reported once
124    /// after the first measure and then only when it changes.
125    FitChanged { target: u32, index: f32 },
126}
127
128impl Event {
129    /// Encode this event as a 16-byte opcode.
130    ///
131    /// `TextChanged` and `Navigate` with a URL cannot be fully encoded here —
132    /// their strings live in the batch's string section / event arena — so this
133    /// emits the opcode with a zero `B` offset; the batch-aware
134    /// `encode_events`/`decode_events` pair in `pathland-core-transport` writes
135    /// the string and its real offset.
136    pub fn encode(&self) -> Opcode {
137        match self {
138            Event::PointerDown {
139                target,
140                x,
141                y,
142                secondary,
143            } => Opcode::new(
144                category::EVENT,
145                crate::event::POINTER_DOWN,
146                flag_for(*secondary, false, false),
147                *target,
148                x.to_bits(),
149                y.to_bits(),
150            ),
151            Event::PointerMove {
152                target,
153                x,
154                y,
155                hovering,
156                leaving,
157            } => Opcode::new(
158                category::EVENT,
159                crate::event::POINTER_MOVE,
160                flag_for(false, *hovering, *leaving),
161                *target,
162                x.to_bits(),
163                y.to_bits(),
164            ),
165            Event::PointerUp {
166                target,
167                x,
168                y,
169                secondary,
170            } => Opcode::new(
171                category::EVENT,
172                crate::event::POINTER_UP,
173                flag_for(*secondary, false, false),
174                *target,
175                x.to_bits(),
176                y.to_bits(),
177            ),
178            Event::KeyDown {
179                target,
180                key_code,
181                modifiers,
182                repeat,
183            } => Opcode::new(
184                category::EVENT,
185                crate::event::KEY_DOWN,
186                if *repeat { flag::KEY_REPEAT } else { 0 },
187                *target,
188                *key_code as u32,
189                *modifiers as u32,
190            ),
191            Event::KeyUp {
192                target,
193                key_code,
194                modifiers,
195            } => Opcode::new(
196                category::EVENT,
197                crate::event::KEY_UP,
198                0,
199                *target,
200                *key_code as u32,
201                *modifiers as u32,
202            ),
203            Event::ValueChanged { target, value } => Opcode::new(
204                category::EVENT,
205                crate::event::VALUE_CHANGED,
206                0,
207                *target,
208                value.to_bits(),
209                0,
210            ),
211            Event::TextChanged { target, .. } => Opcode::new(
212                category::EVENT,
213                crate::event::TEXT_CHANGED,
214                0,
215                *target,
216                0,
217                0,
218            ),
219            Event::FocusChanged { target, focused } => Opcode::new(
220                category::EVENT,
221                crate::event::FOCUS_CHANGED,
222                0,
223                *target,
224                u32::from(*focused),
225                0,
226            ),
227            Event::EditingChanged { target, editing } => Opcode::new(
228                category::EVENT,
229                crate::event::EDITING_CHANGED,
230                0,
231                *target,
232                u32::from(*editing),
233                0,
234            ),
235            Event::Submit { target } => Opcode::new(
236                category::EVENT,
237                crate::event::SUBMIT,
238                0,
239                *target,
240                0,
241                0,
242            ),
243            Event::Scroll {
244                target,
245                offset_x,
246                offset_y,
247            } => Opcode::new(
248                category::EVENT,
249                crate::event::SCROLL,
250                0,
251                *target,
252                offset_x.to_bits(),
253                offset_y.to_bits(),
254            ),
255            Event::Wheel {
256                target,
257                delta_x,
258                delta_y,
259            } => Opcode::new(
260                category::EVENT,
261                crate::event::WHEEL,
262                0,
263                *target,
264                delta_x.to_bits(),
265                delta_y.to_bits(),
266            ),
267            Event::DateChanged {
268                target,
269                days,
270                millis,
271            } => Opcode::new(
272                category::EVENT,
273                crate::event::DATE_CHANGED,
274                0,
275                *target,
276                *days as u32,
277                *millis,
278            ),
279            Event::Navigate { url } => Opcode::new(
280                category::EVENT,
281                crate::event::NAVIGATE,
282                if url.is_some() { flag::NAVIGATE_URL } else { 0 },
283                0,
284                0,
285                0,
286            ),
287            Event::MediaPlayStateChanged { target, playing } => Opcode::new(
288                category::EVENT,
289                crate::event::MEDIA_PLAY_STATE_CHANGED,
290                0,
291                *target,
292                u32::from(*playing),
293                0,
294            ),
295            Event::MediaTimeUpdated { target, seconds } => Opcode::new(
296                category::EVENT,
297                crate::event::MEDIA_TIME_UPDATED,
298                0,
299                *target,
300                seconds.to_bits(),
301                0,
302            ),
303            Event::MediaEnded { target } => Opcode::new(
304                category::EVENT,
305                crate::event::MEDIA_ENDED,
306                0,
307                *target,
308                0,
309                0,
310            ),
311            Event::MediaVolumeChanged { target, volume } => Opcode::new(
312                category::EVENT,
313                crate::event::MEDIA_VOLUME_CHANGED,
314                0,
315                *target,
316                volume.to_bits(),
317                0,
318            ),
319            Event::MediaDurationChanged { target, seconds } => Opcode::new(
320                category::EVENT,
321                crate::event::MEDIA_DURATION_CHANGED,
322                0,
323                *target,
324                seconds.to_bits(),
325                0,
326            ),
327            Event::FitChanged { target, index } => Opcode::new(
328                category::EVENT,
329                crate::event::FIT_CHANGED,
330                0,
331                *target,
332                index.to_bits(),
333                0,
334            ),
335        }
336    }
337}
338
339/// Build a pointer-opcode flags word from the pointer/hover bits.
340#[inline]
341fn flag_for(secondary: bool, hovering: bool, leaving: bool) -> u16 {
342    let mut flags = 0u16;
343    if secondary {
344        flags |= flag::POINTER_SECONDARY;
345    }
346    if hovering {
347        flags |= flag::HOVER_ENTER;
348    }
349    if leaving {
350        flags |= flag::HOVER_LEAVE;
351    }
352    flags
353}
354
355/// Error decoding a raw-input event from an opcode.
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum EventError {
358    /// The opcode is not an `EVENT` category opcode.
359    NotAnEvent,
360    /// The opcode is an EVENT category opcode but an unknown command.
361    UnknownCommand,
362    /// The opcode is `TEXT_CHANGED`, which needs the batch's string section to
363    /// resolve its text; decode it with `pathland-core-transport::decode_events`
364    /// instead of a bare opcode.
365    NeedsStringSection,
366}
367
368impl TryFrom<Opcode> for Event {
369    type Error = EventError;
370
371    fn try_from(op: Opcode) -> Result<Self, Self::Error> {
372        if op.category() != category::EVENT {
373            return Err(EventError::NotAnEvent);
374        }
375        let flags = op.flags();
376        match op.command() {
377            crate::event::POINTER_DOWN => Ok(Event::PointerDown {
378                target: op.a(),
379                x: op.b_f32(),
380                y: op.c_f32(),
381                secondary: flags & flag::POINTER_SECONDARY != 0,
382            }),
383            crate::event::POINTER_MOVE => Ok(Event::PointerMove {
384                target: op.a(),
385                x: op.b_f32(),
386                y: op.c_f32(),
387                hovering: flags & flag::HOVER_ENTER != 0,
388                leaving: flags & flag::HOVER_LEAVE != 0,
389            }),
390            crate::event::POINTER_UP => Ok(Event::PointerUp {
391                target: op.a(),
392                x: op.b_f32(),
393                y: op.c_f32(),
394                secondary: flags & flag::POINTER_SECONDARY != 0,
395            }),
396            crate::event::KEY_DOWN => Ok(Event::KeyDown {
397                target: op.a(),
398                key_code: op.b() as u16,
399                modifiers: op.c() as u8,
400                repeat: flags & flag::KEY_REPEAT != 0,
401            }),
402            crate::event::KEY_UP => Ok(Event::KeyUp {
403                target: op.a(),
404                key_code: op.b() as u16,
405                modifiers: op.c() as u8,
406            }),
407            crate::event::VALUE_CHANGED => Ok(Event::ValueChanged {
408                target: op.a(),
409                value: op.b_f32(),
410            }),
411            crate::event::FOCUS_CHANGED => Ok(Event::FocusChanged {
412                target: op.a(),
413                focused: op.b() != 0,
414            }),
415            crate::event::EDITING_CHANGED => Ok(Event::EditingChanged {
416                target: op.a(),
417                editing: op.b() != 0,
418            }),
419            crate::event::SUBMIT => Ok(Event::Submit { target: op.a() }),
420            crate::event::SCROLL => Ok(Event::Scroll {
421                target: op.a(),
422                offset_x: op.b_f32(),
423                offset_y: op.c_f32(),
424            }),
425            crate::event::WHEEL => Ok(Event::Wheel {
426                target: op.a(),
427                delta_x: op.b_f32(),
428                delta_y: op.c_f32(),
429            }),
430            crate::event::DATE_CHANGED => Ok(Event::DateChanged {
431                target: op.a(),
432                days: op.b() as i32,
433                millis: op.c(),
434            }),
435            crate::event::TEXT_CHANGED => Err(EventError::NeedsStringSection),
436            crate::event::NAVIGATE => {
437                if flags & flag::NAVIGATE_URL != 0 {
438                    Err(EventError::NeedsStringSection)
439                } else {
440                    Ok(Event::Navigate { url: None })
441                }
442            }
443            crate::event::MEDIA_PLAY_STATE_CHANGED => Ok(Event::MediaPlayStateChanged {
444                target: op.a(),
445                playing: op.b() != 0,
446            }),
447            crate::event::MEDIA_TIME_UPDATED => Ok(Event::MediaTimeUpdated {
448                target: op.a(),
449                seconds: op.b_f32(),
450            }),
451            crate::event::MEDIA_ENDED => Ok(Event::MediaEnded { target: op.a() }),
452            crate::event::MEDIA_VOLUME_CHANGED => Ok(Event::MediaVolumeChanged {
453                target: op.a(),
454                volume: op.b_f32(),
455            }),
456            crate::event::MEDIA_DURATION_CHANGED => Ok(Event::MediaDurationChanged {
457                target: op.a(),
458                seconds: op.b_f32(),
459            }),
460            crate::event::FIT_CHANGED => Ok(Event::FitChanged {
461                target: op.a(),
462                index: op.b_f32(),
463            }),
464            _ => Err(EventError::UnknownCommand),
465        }
466    }
467}
468
469/// Decode an event opcode for the **shared-memory path**, resolving
470/// `TEXT_CHANGED` text and `NAVIGATE` URLs from the host → guest event arena
471/// (`B` is an absolute offset into that region). Non-string events decode as
472/// usual.
473///
474/// Returns `None` for non-event opcodes, unknown commands, or string events
475/// whose offset cannot be resolved (invalid or out of range).
476pub fn decode_event(op: &Opcode, event_arena: &[u8]) -> Option<Event> {
477    if op.category() != category::EVENT {
478        return None;
479    }
480    if op.command() == crate::event::TEXT_CHANGED {
481        let text = crate::arena::get_str(event_arena, op.b()).ok()?;
482        return Some(Event::TextChanged {
483            target: op.a(),
484            value: text.to_string(),
485        });
486    }
487    if op.command() == crate::event::NAVIGATE && op.flags() & flag::NAVIGATE_URL != 0 {
488        let url = crate::arena::get_str(event_arena, op.b()).ok()?;
489        return Some(Event::Navigate {
490            url: Some(url.to_string()),
491        });
492    }
493    Event::try_from(*op).ok()
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    fn round_trip(e: Event) -> Event {
501        Event::try_from(Opcode::from_bytes(&e.encode().to_bytes())).unwrap()
502    }
503
504    #[test]
505    fn pointer_down_round_trips() {
506        let e = Event::PointerDown {
507            target: 5,
508            x: 10.0,
509            y: 20.0,
510            secondary: true,
511        };
512        assert_eq!(round_trip(e.clone()), e);
513    }
514
515    #[test]
516    fn pointer_move_round_trips_with_hover_flags() {
517        let e = Event::PointerMove {
518            target: 5,
519            x: 10.0,
520            y: 20.0,
521            hovering: true,
522            leaving: false,
523        };
524        assert_eq!(round_trip(e.clone()), e);
525
526        let leaving = Event::PointerMove {
527            target: 5,
528            x: 10.0,
529            y: 20.0,
530            hovering: false,
531            leaving: true,
532        };
533        assert_eq!(round_trip(leaving.clone()), leaving);
534    }
535
536    #[test]
537    fn pointer_up_round_trips() {
538        let e = Event::PointerUp {
539            target: 5,
540            x: 10.0,
541            y: 20.0,
542            secondary: false,
543        };
544        assert_eq!(round_trip(e.clone()), e);
545    }
546
547    #[test]
548    fn key_down_round_trips_with_repeat() {
549        let e = Event::KeyDown {
550            target: 5,
551            key_code: 0x41,
552            modifiers: 0x01,
553            repeat: true,
554        };
555        assert_eq!(round_trip(e.clone()), e);
556    }
557
558    #[test]
559    fn key_up_round_trips() {
560        let e = Event::KeyUp {
561            target: 5,
562            key_code: 0x41,
563            modifiers: 0x01,
564        };
565        assert_eq!(round_trip(e.clone()), e);
566    }
567
568    #[test]
569    fn value_changed_round_trips() {
570        let e = Event::ValueChanged {
571            target: 7,
572            value: 0.625,
573        };
574        assert_eq!(round_trip(e.clone()), e);
575    }
576
577    #[test]
578    fn draft_events_round_trip() {
579        for e in [
580            Event::FocusChanged { target: 1, focused: true },
581            Event::FocusChanged { target: 1, focused: false },
582            Event::EditingChanged { target: 2, editing: true },
583            Event::Submit { target: 3 },
584            Event::Scroll { target: 4, offset_x: 10.0, offset_y: 20.5 },
585            Event::Wheel { target: 5, delta_x: -1.5, delta_y: 2.0 },
586            Event::DateChanged { target: 6, days: 19_723, millis: 3_600_000 },
587            Event::DateChanged { target: 6, days: -1, millis: 0 },
588        ] {
589            assert_eq!(round_trip(e.clone()), e);
590        }
591    }
592
593    #[test]
594    fn media_events_round_trip() {
595        for e in [
596            Event::MediaPlayStateChanged { target: 7, playing: true },
597            Event::MediaPlayStateChanged { target: 7, playing: false },
598            Event::MediaTimeUpdated { target: 7, seconds: 12.5 },
599            Event::MediaEnded { target: 7 },
600            Event::MediaVolumeChanged { target: 7, volume: 0.5 },
601            Event::MediaDurationChanged { target: 7, seconds: 210.0 },
602            Event::FitChanged { target: 42, index: 2.0 },
603        ] {
604            assert_eq!(round_trip(e.clone()), e);
605        }
606        // Conformance vector bytes (spec/CONFORMANCE.md vectors 23–26, 26a).
607        assert_eq!(
608            Event::MediaPlayStateChanged { target: 7, playing: true }.encode().to_bytes(),
609            [0x03, 0x0F, 0, 0, 7, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]
610        );
611        assert_eq!(
612            Event::MediaTimeUpdated { target: 7, seconds: 12.5 }.encode().to_bytes(),
613            [0x03, 0x10, 0, 0, 7, 0, 0, 0, 0, 0, 0x48, 0x41, 0, 0, 0, 0]
614        );
615        assert_eq!(
616            Event::MediaEnded { target: 7 }.encode().to_bytes(),
617            [0x03, 0x11, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
618        );
619        assert_eq!(
620            Event::MediaVolumeChanged { target: 7, volume: 0.5 }.encode().to_bytes(),
621            [0x03, 0x12, 0, 0, 7, 0, 0, 0, 0, 0, 0x00, 0x3F, 0, 0, 0, 0]
622        );
623        assert_eq!(
624            Event::MediaDurationChanged { target: 7, seconds: 210.0 }.encode().to_bytes(),
625            [0x03, 0x14, 0, 0, 7, 0, 0, 0, 0, 0, 0x52, 0x43, 0, 0, 0, 0]
626        );
627        assert_eq!(
628            Event::FitChanged { target: 42, index: 2.0 }.encode().to_bytes(),
629            [0x03, 0x13, 0, 0, 0x2A, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0, 0]
630        );
631    }
632
633    #[test]
634    fn non_event_opcode_is_rejected() {
635        let op = Opcode::new(crate::category::TREE, 0x01, 0, 1, 0, 0);
636        assert_eq!(Event::try_from(op), Err(EventError::NotAnEvent));
637    }
638
639    #[test]
640    fn unknown_event_command_is_rejected() {
641        // EVENT category with a command that does not exist in the protocol.
642        let op = Opcode::new(crate::category::EVENT, 0xFF, 0, 1, 0, 0);
643        assert_eq!(Event::try_from(op), Err(EventError::UnknownCommand));
644    }
645
646    #[test]
647    fn text_changed_needs_string_section() {
648        // `TextChanged` is only decodable from a batch (its text lives in the
649        // string section); a bare opcode cannot resolve it.
650        let op = Opcode::new(crate::category::EVENT, crate::event::TEXT_CHANGED, 0, 7, 0, 0);
651        assert_eq!(Event::try_from(op), Err(EventError::NeedsStringSection));
652    }
653
654    #[test]
655    fn navigate_back_round_trips() {
656        let e = Event::Navigate { url: None };
657        assert_eq!(round_trip(e.clone()), e);
658    }
659
660    #[test]
661    fn navigate_with_url_needs_string_section() {
662        // `Navigate` with a URL is only decodable where the URL can be resolved
663        // (batch string section / shared-memory event arena); a bare opcode
664        // cannot resolve it.
665        let op = Opcode::new(
666            crate::category::EVENT,
667            crate::event::NAVIGATE,
668            flag::NAVIGATE_URL,
669            0,
670            0,
671            0,
672        );
673        assert_eq!(Event::try_from(op), Err(EventError::NeedsStringSection));
674    }
675
676    #[test]
677    fn navigate_back_matches_conformance_vector_bytes() {
678        // EVENT:NAVIGATE (back, no URL) — vector 22.
679        let e = Event::Navigate { url: None };
680        assert_eq!(
681            e.encode().to_bytes(),
682            [0x03, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
683        );
684    }
685
686    #[test]
687    fn decode_event_resolves_navigate_url_from_arena() {
688        // The shared-memory path resolves a `Navigate` URL from the event arena.
689        use alloc::vec;
690        let mut arena = vec![0u8; 32];
691        // "[u32 len=9][/users/42]" at offset 0.
692        arena[0..4].copy_from_slice(&9u32.to_le_bytes());
693        arena[4..13].copy_from_slice(b"/users/42");
694        let op = Opcode::new(
695            crate::category::EVENT,
696            crate::event::NAVIGATE,
697            flag::NAVIGATE_URL,
698            0,
699            0,
700            0,
701        );
702        assert_eq!(
703            decode_event(&op, &arena),
704            Some(Event::Navigate {
705                url: Some("/users/42".into())
706            })
707        );
708    }
709
710    #[test]
711    fn pointer_move_round_trips_with_combined_hover_flags() {
712        // Both hovering and leaving set simultaneously must survive the round trip.
713        let e = Event::PointerMove {
714            target: 5,
715            x: 10.0,
716            y: 20.0,
717            hovering: true,
718            leaving: true,
719        };
720        assert_eq!(round_trip(e.clone()), e);
721        // The flags word carries both bits.
722        assert_eq!(e.encode().flags(), flag::HOVER_ENTER | flag::HOVER_LEAVE);
723    }
724
725    #[test]
726    fn matches_conformance_vector_bytes() {
727        // EVENT:POINTER_DOWN (target=5, x=10.0, y=20.0)
728        let down = Event::PointerDown {
729            target: 5,
730            x: 10.0,
731            y: 20.0,
732            secondary: false,
733        };
734        assert_eq!(
735            down.encode().to_bytes(),
736            [
737                0x03, 0x01, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x41, 0x00,
738                0x00, 0xA0, 0x41,
739            ]
740        );
741    }
742}