event_types 0.1.0

Types to help idiomatically represent user input events
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
use crate::coordinate_space::{
    ClientSpace, ElementSpace, Lines, PageSpace, Pages, Pixels, ScreenSpace,
};
use euclid::num::Zero;
use euclid::*;

/// A point in [ScreenSpace]
pub type ScreenPoint = Point2D<f64, ScreenSpace>;

/// A point in [PageSpace]
pub type PagePoint = Point2D<f64, PageSpace>;

/// A point in [ClientSpace]
pub type ClientPoint = Point2D<f64, ClientSpace>;

/// A point in [ElementSpace]
pub type ElementPoint = Point2D<f64, ElementSpace>;

/// A vector expressed in [Pixels]
pub type PixelsVector = Vector3D<f64, Pixels>;

/// A vector expressed in [Lines]
pub type LinesVector = Vector3D<f64, Lines>;

/// A vector expressed in [Pages]
pub type PagesVector = Vector3D<f64, Pages>;

/// A vector representing the movement of the mouse wheel
///
/// This may be expressed in [Pixels], [Lines] or [Pages]
#[derive(Copy, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum WheelDelta {
    /// Movement in Pixels
    Pixels(PixelsVector),
    /// Movement in Lines
    Lines(LinesVector),
    /// Movement in Pages
    Pages(PagesVector),
}

impl Zero for WheelDelta {
    fn zero() -> Self {
        WheelDelta::pixels(0., 0., 0.)
    }
}

impl WheelDelta {
    /// Convenience function for constructing a WheelDelta with pixel units
    pub fn pixels(x: f64, y: f64, z: f64) -> Self {
        WheelDelta::Pixels(PixelsVector::new(x, y, z))
    }

    /// Convenience function for constructing a WheelDelta with line units
    pub fn lines(x: f64, y: f64, z: f64) -> Self {
        WheelDelta::Lines(LinesVector::new(x, y, z))
    }

    /// Convenience function for constructing a WheelDelta with page units
    pub fn pages(x: f64, y: f64, z: f64) -> Self {
        WheelDelta::Pages(PagesVector::new(x, y, z))
    }

    /// Returns true iff there is no wheel movement
    ///
    /// i.e. the x, y and z delta is zero (disregarding units)
    pub fn is_zero(&self) -> bool {
        self.strip_units() == Vector3D::zero()
    }

    /// A Vector3D proportional to the amount scrolled
    ///
    /// Note that this disregards the 3 possible units: this could be expressed in terms of pixels, lines, or pages.
    ///
    /// In most cases, to properly handle scrolling, you should handle all 3 possible enum variants instead of stripping units. Otherwise, if you assume that the units will always be pixels, the user may experience some unexpectedly slow scrolling if their mouse/OS sends values expressed in lines or pages.
    pub fn strip_units(&self) -> Vector3D<f64, UnknownUnit> {
        match self {
            WheelDelta::Pixels(v) => v.cast_unit(),
            WheelDelta::Lines(v) => v.cast_unit(),
            WheelDelta::Pages(v) => v.cast_unit(),
        }
    }
}

#[cfg(test)]
mod wheel_delta {
    use super::*;

    #[test]
    fn zero() {
        let d = WheelDelta::zero();
        assert_eq!(d, WheelDelta::Pixels(PixelsVector::new(0., 0., 0.,)));
        assert!(d.is_zero());
    }

    #[test]
    fn construct_pixels() {
        let d = WheelDelta::pixels(1., 2., 3.);
        assert_eq!(d, WheelDelta::Pixels(PixelsVector::new(1., 2., 3.,)));
    }

    #[test]
    fn construct_lines() {
        let d = WheelDelta::lines(1., 2., 3.);
        assert_eq!(d, WheelDelta::Lines(LinesVector::new(1., 2., 3.,)));
    }

    #[test]
    fn construct_pages() {
        let d = WheelDelta::pages(1., 2., 3.);
        assert_eq!(d, WheelDelta::Pages(PagesVector::new(1., 2., 3.,)));
    }

    #[test]
    fn strip_units() {
        let d = WheelDelta::pixels(1., 2., 3.);
        assert_eq!(d.strip_units(), Vector3D::new(1., 2., 3.));
    }
}

/// Coordinates of a point in a user interface
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Coordinates {
    screen: ScreenPoint,
    page: PagePoint,
    client: ClientPoint,
    element: ElementPoint,
}

impl Coordinates {
    /// Construct new coordinates with the specified screen-, client-, element- and page-relative points
    pub fn new(
        screen: ScreenPoint,
        page: PagePoint,
        client: ClientPoint,
        element: ElementPoint,
    ) -> Self {
        Self {
            screen,
            page,
            client,
            element,
        }
    }
    /// Coordinates relative to the entire screen. This takes into account the window's offset.
    pub fn screen(&self) -> ScreenPoint {
        self.screen
    }

    /// Coordinates relative to the entire document. This includes any portion of the document not currently visible.
    ///
    /// For example, if the page is scrolled 200 pixels to the right and 300 pixels down, the top left corner of the viewport corresponds to page coordinates (200., 300.)
    pub fn page(&self) -> PagePoint {
        self.page
    }

    /// Coordinates relative to the application's viewport (as opposed to the coordinates within the page).
    ///
    /// For example, the top left corner of the viewport corresponds to client coordinates (0., 0.), regardless of whether the page is scrolled.
    pub fn client(&self) -> ClientPoint {
        self.client
    }

    /// Coordinates relative to the top-left corner of the target element
    ///
    /// For example, the top left corner of an element corresponds to element coordinates (0., 0.)
    pub fn element(&self) -> ElementPoint {
        self.element
    }
}

impl Zero for Coordinates {
    fn zero() -> Self {
        Self::new(
            ScreenPoint::zero(),
            PagePoint::zero(),
            ClientPoint::zero(),
            ElementPoint::zero(),
        )
    }
}

#[cfg(test)]
mod coordinates {
    use super::*;

    #[test]
    fn getters() {
        let c = Coordinates::new(
            ScreenPoint::new(1., 1.),
            PagePoint::new(2., 2.),
            ClientPoint::new(3., 3.),
            ElementPoint::new(4., 4.),
        );

        assert_eq!(c.screen(), ScreenPoint::new(1., 1.));
        assert_eq!(c.page(), PagePoint::new(2., 2.));
        assert_eq!(c.client(), ClientPoint::new(3., 3.));
        assert_eq!(c.element(), ElementPoint::new(4., 4.));
    }
}

/// The orientation (tilt or spherical angles, and twist) of a transducer (e.g. pen/stylus)
#[derive(PartialEq, Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PointerOrientation {
    tilt_x: Angle<f64>,
    tilt_y: Angle<f64>,

    altitude: Angle<f64>,
    azimuth: Angle<f64>,

    twist: Angle<f64>,
}

impl PointerOrientation {
    /// Construct a new PointerAngle by specifying its tilt, spherical angle and twist
    pub fn new(
        tilt_x: Angle<f64>,
        tilt_y: Angle<f64>,
        altitude: Angle<f64>,
        azimuth: Angle<f64>,
        twist: Angle<f64>,
    ) -> Self {
        Self {
            tilt_x,
            tilt_y,
            altitude,
            azimuth,
            twist,
        }
    }

    /// Construct a new PointerAngle by specifying the tilt angles and twist
    ///
    /// Spherical angles (altitude and azimuth) will be calculated automatically
    pub fn from_tilt_and_twist(tilt_x: Angle<f64>, tilt_y: Angle<f64>, twist: Angle<f64>) -> Self {
        let (altitude, azimuth) = tilt_to_spherical(tilt_x, tilt_y);

        Self {
            tilt_x,
            tilt_y,
            altitude,
            azimuth,
            twist,
        }
    }

    /// Construct a new PointerAngle by specifying the spherical angles and twist
    ///
    /// Tilt angles (tilt_x and tilt_y) will be calculated automatically
    pub fn from_spherical_and_twist(
        altitude: Angle<f64>,
        azimuth: Angle<f64>,
        twist: Angle<f64>,
    ) -> Self {
        let (tilt_x, tilt_y) = spherical_to_tilt(altitude, azimuth);

        Self {
            tilt_x,
            tilt_y,
            altitude,
            azimuth,
            twist,
        }
    }

    /// The plane angle (in the range of \[-PI/2,PI/2\]) between the Y-Z plane and the plane containing both a transducer (e.g. pen/stylus) axis and the Y axis.
    ///
    /// A positive value is to the right, in the direction of increasing X values. For hardware and platforms that do not report tilt, the value must be zero.
    pub fn tilt_x(&self) -> Angle<f64> {
        self.tilt_x
    }

    /// The plane angle in the range of \[-PI/2,PI/2\]) between the X-Z plane and the plane containing both a transducer (e.g. pen/stylus) axis and the X axis.
    ///
    /// A positive value is towards the user, in the direction of increasing Y values. For hardware and platforms that do not report tilt, the value must be zero.
    pub fn tilt_y(&self) -> Angle<f64> {
        self.tilt_y
    }

    /// The altitude of a transducer (e.g. pen/stylus), in the range \[0,Ï€/2\]
    ///
    /// 0 is parallel to the surface (X-Y plane), and π/2 is perpendicular to the surface. For hardware and platforms that do not report tilt or angle, the value must be π/2.
    pub fn altitude(&self) -> Angle<f64> {
        self.altitude
    }

    /// The azimuth angle of a transducer (e.g. pen/stylus), in the range [0, 2Ï€]
    ///
    /// 0 represents a transducer whose cap is pointing in the direction of increasing X values (point to "3 o'clock" if looking straight down) on the X-Y plane, and the values progressively increase when going clockwise (π/2 at "6 o'clock", π at "9 o'clock", 3π/2 at "12 o'clock").
    ///
    /// When a transducer is perfectly perpendicular to the surface (altitudeAngle of π/2), the value must be 0. For hardware and platforms that do not report tilt or angle, the value MUST be 0.
    pub fn azimuth(&self) -> Angle<f64> {
        self.azimuth
    }

    /// The clockwise rotation of a transducer (e.g. pen/stylus) around its own major axis, in the range \[0-2Ï€\]
    ///
    /// For hardware and platforms that do not report twist, the value MUST be 0.
    pub fn twist(&self) -> Angle<f64> {
        self.twist
    }
}

impl Default for PointerOrientation {
    /// Default orientation: perpendicular to the surface
    ///
    /// All angles are zero except for altitude, which is π/2
    fn default() -> Self {
        Self::new(
            Angle::zero(),
            Angle::zero(),
            Angle::frac_pi_2(),
            Angle::zero(),
            Angle::zero(),
        )
    }
}

#[cfg(test)]
mod pointer_orientation {
    use super::*;
    use assert2::assert;
    use euclid::approxeq::ApproxEq;

    #[test]
    fn default() {
        let p = PointerOrientation::default();

        assert!(p.azimuth() == Angle::zero());
        assert!(p.altitude() == Angle::frac_pi_2());
        assert!(p.tilt_x() == Angle::zero());
        assert!(p.tilt_y() == Angle::zero());
        assert!(p.twist() == Angle::zero());
    }

    #[test]
    fn twist() {
        let p = PointerOrientation::from_tilt_and_twist(
            Angle::zero(),
            Angle::zero(),
            Angle::degrees(42.),
        );
        assert!(p.twist() == Angle::degrees(42.));

        let p = PointerOrientation::from_spherical_and_twist(
            Angle::zero(),
            Angle::zero(),
            Angle::degrees(42.),
        );
        assert!(p.twist() == Angle::degrees(42.));
    }

    #[test]
    fn from_tilt() {
        let p =
            PointerOrientation::from_tilt_and_twist(Angle::zero(), Angle::zero(), Angle::default());
        assert!(p.azimuth() == Angle::zero());
        assert!(p.altitude() == Angle::frac_pi_2());
        assert!(p.tilt_x() == Angle::zero());
        assert!(p.tilt_y() == Angle::zero());

        let p = PointerOrientation::from_tilt_and_twist(
            Angle::frac_pi_4(),
            Angle::zero(),
            Angle::default(),
        );
        assert!(p.azimuth() == Angle::zero());
        assert!(p.altitude() == Angle::frac_pi_4());
        assert!(p.tilt_x() == Angle::frac_pi_4());
        assert!(p.tilt_y() == Angle::zero());

        let p = PointerOrientation::from_tilt_and_twist(
            Angle::zero(),
            Angle::frac_pi_4(),
            Angle::default(),
        );
        assert!(p.azimuth() == Angle::frac_pi_2());
        assert!(p.altitude() == Angle::frac_pi_4());
        assert!(p.tilt_x() == Angle::zero());
        assert!(p.tilt_y() == Angle::frac_pi_4());

        let p = PointerOrientation::from_tilt_and_twist(
            -Angle::frac_pi_4(),
            Angle::zero(),
            Angle::default(),
        );
        assert!(p.azimuth() == Angle::pi());
        assert!(p.altitude() == Angle::frac_pi_4());
        assert!(p.tilt_x() == -Angle::frac_pi_4());
        assert!(p.tilt_y() == Angle::zero());

        // Example produced with GeoGebra calculator
        // https://www.geogebra.org/calculator/ejgtzkyd
        let p = PointerOrientation::from_tilt_and_twist(
            Angle::degrees(70.),
            Angle::degrees(30.),
            Angle::default(),
        );
        assert!(p.altitude().to_degrees().approx_eq_eps(&19.61, &0.1));
        // crazy loss of precision idk
        assert!(p.azimuth().to_degrees().approx_eq_eps(&11.17, &1.));
    }

    #[test]
    fn from_spherical() {
        let p = PointerOrientation::from_spherical_and_twist(
            Angle::frac_pi_4(),
            Angle::zero(),
            Angle::default(),
        );
        assert!(p.tilt_x().approx_eq(&Angle::frac_pi_4()));
        assert!(p.tilt_y().approx_eq(&Angle::zero()));

        let p = PointerOrientation::from_spherical_and_twist(
            Angle::frac_pi_4(),
            Angle::frac_pi_2(),
            Angle::default(),
        );
        assert!(p.tilt_x().approx_eq(&Angle::zero()));
        assert!(p.tilt_y().approx_eq(&Angle::frac_pi_4()));

        let p = PointerOrientation::from_spherical_and_twist(
            Angle::frac_pi_4(),
            Angle::pi(),
            Angle::default(),
        );
        assert!(p.tilt_x().approx_eq(&-Angle::frac_pi_4()));
        assert!(p.tilt_y().approx_eq(&Angle::zero()));

        // Example produced with GeoGebra calculator
        // https://www.geogebra.org/calculator/ejgtzkyd
        let p = PointerOrientation::from_spherical_and_twist(
            Angle::degrees(19.61),
            Angle::degrees(11.17),
            Angle::default(),
        );
        assert!(p.tilt_x().to_degrees().approx_eq_eps(&70., &0.1));
        // crazy loss of precision?
        assert!(p.tilt_y().to_degrees().approx_eq_eps(&30., &2.));
    }
}

/// Convert spherical angles (altitude + azimuth) to tilt (tilt X & tilt Y)
///
/// According to web spec https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
fn spherical_to_tilt(altitude: Angle<f64>, azimuth: Angle<f64>) -> (Angle<f64>, Angle<f64>) {
    use std::f64::consts::{FRAC_PI_2 as HALF_PI, PI};
    const THREE_HALF_PI: f64 = PI / 2. * 3.;
    const TWO_PI: f64 = 2. * PI;

    if altitude == Angle::zero() {
        let a = azimuth.radians;
        let (tilt_x, tilt_y) = if a == 0. || a == TWO_PI {
            (HALF_PI, 0.)
        } else if 0. < a && a < HALF_PI {
            (HALF_PI, HALF_PI)
        } else if a == HALF_PI {
            (0., HALF_PI)
        } else if HALF_PI < a && a < PI {
            (-HALF_PI, HALF_PI)
        } else if a == PI {
            (-HALF_PI, 0.)
        } else if PI < a && a < THREE_HALF_PI {
            (-HALF_PI, -HALF_PI)
        } else if a == THREE_HALF_PI {
            (0., -HALF_PI)
        } else if THREE_HALF_PI < a && a < TWO_PI {
            (HALF_PI, -HALF_PI)
        } else {
            panic!("invalid value for azimuth {azimuth:?}")
        };

        (Angle::radians(tilt_x), Angle::radians(tilt_y))
    } else {
        let altitude_tan = altitude.radians.tan();
        let (azimuth_sin, azimuth_cos) = azimuth.sin_cos();

        let tilt_x = (azimuth_cos / altitude_tan).atan();
        let tilt_y = (azimuth_sin / altitude_tan).atan();

        (Angle::radians(tilt_x), Angle::radians(tilt_y))
    }
}

/// Convert tilt angles (tilt X & tilt Y) to spherical (altitude + azimuth)
///
/// According to web spec https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
fn tilt_to_spherical(tilt_x: Angle<f64>, tilt_y: Angle<f64>) -> (Angle<f64>, Angle<f64>) {
    use std::f64::consts::{FRAC_PI_2 as HALF_PI, PI};
    const THREE_HALF_PI: f64 = PI / 2. * 3.;
    const TWO_PI: f64 = 2. * PI;

    let x = tilt_x.radians;
    let y = tilt_y.radians;

    let azimuth = if x == 0. {
        if y < 0. {
            THREE_HALF_PI
        } else if y > 0. {
            HALF_PI
        } else {
            0.
        }
    } else if y == 0. {
        if x < 0. {
            PI
        } else {
            0.
        }
    } else if x.abs() == HALF_PI || y.abs() == HALF_PI {
        // not enough information to calculate azimuth
        0.
    } else {
        let tan_x = x.tan();
        let tan_y = y.tan();

        let a = tan_y.atan2(tan_x);

        if a < 0. {
            a + TWO_PI
        } else {
            a
        }
    };

    let altitude = if x.abs() == HALF_PI || y.abs() == HALF_PI {
        0.
    } else if x == 0. {
        HALF_PI - y.abs()
    } else if y == 0. {
        HALF_PI - x.abs()
    } else {
        let x_tan_square = x.tan().powi(2);
        let y_tan_square = y.tan().powi(2);

        (1. / (x_tan_square + y_tan_square).sqrt()).atan()
    };

    (Angle::radians(altitude), Angle::radians(azimuth))
}