1use alloc::string::String;
12use alloc::string::ToString;
13use crate::category;
14use crate::flag;
15use crate::Opcode;
16
17#[derive(Debug, Clone, PartialEq)]
26pub enum Event {
27 PointerDown {
29 target: u32,
30 x: f32,
31 y: f32,
32 secondary: bool,
33 },
34 PointerMove {
36 target: u32,
37 x: f32,
38 y: f32,
39 hovering: bool,
40 leaving: bool,
41 },
42 PointerUp {
44 target: u32,
45 x: f32,
46 y: f32,
47 secondary: bool,
48 },
49 KeyDown {
51 target: u32,
52 key_code: u16,
53 modifiers: u8,
54 repeat: bool,
55 },
56 KeyUp {
58 target: u32,
59 key_code: u16,
60 modifiers: u8,
61 },
62 ValueChanged {
66 target: u32,
67 value: f32,
68 },
69 TextChanged {
72 target: u32,
73 value: String,
74 },
75 FocusChanged { target: u32, focused: bool },
77 EditingChanged { target: u32, editing: bool },
79 Submit { target: u32 },
81 Scroll {
83 target: u32,
84 offset_x: f32,
85 offset_y: f32,
86 },
87 Wheel {
89 target: u32,
90 delta_x: f32,
91 delta_y: f32,
92 },
93 DateChanged {
96 target: u32,
97 days: i32,
98 millis: u32,
99 },
100 Navigate { url: Option<String> },
105 MediaPlayStateChanged { target: u32, playing: bool },
108 MediaTimeUpdated { target: u32, seconds: f32 },
112 MediaEnded { target: u32 },
115 MediaVolumeChanged { target: u32, volume: f32 },
118 MediaDurationChanged { target: u32, seconds: f32 },
122 FitChanged { target: u32, index: f32 },
126}
127
128impl Event {
129 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum EventError {
358 NotAnEvent,
360 UnknownCommand,
362 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
469pub 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 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 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 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 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 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 use alloc::vec;
690 let mut arena = vec![0u8; 32];
691 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 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 assert_eq!(e.encode().flags(), flag::HOVER_ENTER | flag::HOVER_LEAVE);
723 }
724
725 #[test]
726 fn matches_conformance_vector_bytes() {
727 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}