Skip to main content

blockworx_canvas2d/input/
dom.rs

1//! The one wasm-bound layer of the input path: a DOM event, as a sample.
2
3use blockworx_geom::{Pos2, pos2};
4use blockworx_paint::{Button, PointerKind, ScrollPx};
5use web_sys::{DragEvent, KeyboardEvent, PointerEvent, WheelEvent};
6
7use super::{Edge, Held, KeySample, Named, Phase, PointerId, PointerSample, WheelSample};
8
9/// The pointer a drag is carried by. A browser runs a file drag itself and
10/// raises no pointer events for it, so the motion this reports belongs to no
11/// real device and takes an id no device will claim.
12const DRAGGED: PointerId = PointerId(-1);
13
14/// `WheelEvent.deltaMode` counts lines rather than pixels; Firefox reports
15/// wheels this way. epaint uses the same nominal row height.
16const PIXELS_PER_LINE: f32 = 24.0;
17
18/// And pages, for the rare host that reports those.
19const LINES_PER_PAGE: f32 = 20.0;
20
21/// One pointer event as the listener that caught it names its phase.
22/// `origin` is the canvas's top-left in client coordinates, so the sample is
23/// in the canvas's own CSS pixels.
24#[must_use]
25pub fn pointer(event: &PointerEvent, phase: Phase, origin: Pos2) -> PointerSample {
26    PointerSample {
27        id: PointerId(event.pointer_id()),
28        at: at(event.client_x(), event.client_y(), origin),
29        phase,
30        button: matches!(phase, Phase::Down | Phase::Up)
31            .then(|| button_of(event.button()))
32            .flatten(),
33        held: held(event.buttons()),
34        kind: if event.pointer_type() == "touch" {
35            PointerKind::Touch
36        } else {
37            PointerKind::Mouse
38        },
39        shift: event.shift_key().into(),
40    }
41}
42
43/// Where a file being dragged over the canvas is, as a motion.
44///
45/// The browser reserves the pointer for the drag it is running, so nothing
46/// else says where the drop is about to land — and without this the editor
47/// would place what arrives wherever the pointer was last seen, before the
48/// drag began.
49#[must_use]
50pub fn dragged(event: &DragEvent, origin: Pos2) -> PointerSample {
51    PointerSample {
52        id: DRAGGED,
53        at: at(event.client_x(), event.client_y(), origin),
54        phase: Phase::Moved,
55        button: None,
56        held: Held::NONE,
57        kind: PointerKind::Mouse,
58        shift: event.shift_key().into(),
59    }
60}
61
62#[must_use]
63pub fn wheel(event: &WheelEvent, origin: Pos2) -> WheelSample {
64    WheelSample {
65        at: at(event.client_x(), event.client_y(), origin),
66        // The DOM counts a wheel rolled *away* from the user as negative, and
67        // that is the notch that magnifies.
68        scroll: ScrollPx::up(-pixels(event.delta_y() as f32, event.delta_mode())),
69        shift: event.shift_key().into(),
70    }
71}
72
73/// The canvas's own reading of `event`, or `None` for a key it does not read
74/// — which may still be a [`Chord`](blockworx_paint::Chord); see
75/// [`chord_of`](super::chord_of).
76#[must_use]
77pub fn key(event: &KeyboardEvent, edge: Edge) -> Option<KeySample> {
78    let named = match event.key().as_str() {
79        "Escape" => Named::Escape,
80        "Delete" | "Backspace" => Named::Delete,
81        " " | "Spacebar" => Named::Space,
82        _ => return None,
83    };
84    Some(KeySample {
85        key: named,
86        edge,
87        shift: event.shift_key().into(),
88    })
89}
90
91fn at(client_x: i32, client_y: i32, origin: Pos2) -> Pos2 {
92    pos2(client_x as f32 - origin.x, client_y as f32 - origin.y)
93}
94
95fn held(buttons: u16) -> Held {
96    Held::of(
97        [
98            (1, Button::Primary),
99            (2, Button::Secondary),
100            (4, Button::Middle),
101        ]
102        .into_iter()
103        .filter(|&(mask, _)| buttons & mask != 0)
104        .map(|(_, button)| button),
105    )
106}
107
108/// Which button a `MouseEvent.button` names. Public because a host has to
109/// know one before the sample is made: a button the camera drives must also
110/// be refused whatever the platform binds it to.
111#[must_use]
112pub fn button_of(button: i16) -> Option<Button> {
113    match button {
114        0 => Some(Button::Primary),
115        1 => Some(Button::Middle),
116        2 => Some(Button::Secondary),
117        _ => None,
118    }
119}
120
121fn pixels(delta: f32, mode: u32) -> f32 {
122    match mode {
123        1 => delta * PIXELS_PER_LINE,
124        2 => delta * PIXELS_PER_LINE * LINES_PER_PAGE,
125        _ => delta,
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn the_dom_button_mask_names_the_buttons_it_holds() {
135        assert_eq!(held(0), Held::NONE);
136        assert_eq!(held(1), Held::of([Button::Primary]));
137        assert_eq!(held(5), Held::of([Button::Primary, Button::Middle]));
138        assert!(held(2).holds(Button::Secondary));
139    }
140
141    /// The DOM's button *index* is not its bitmask position: the middle
142    /// button is 1 there and 4 here.
143    #[test]
144    fn the_dom_button_index_is_not_the_mask() {
145        assert_eq!(button_of(0), Some(Button::Primary));
146        assert_eq!(button_of(1), Some(Button::Middle));
147        assert_eq!(button_of(2), Some(Button::Secondary));
148        assert_eq!(button_of(3), None);
149    }
150
151    #[test]
152    fn a_wheel_reported_in_lines_or_pages_is_read_in_pixels() {
153        assert_eq!(pixels(3.0, 0), 3.0);
154        assert_eq!(pixels(3.0, 1), 3.0 * PIXELS_PER_LINE);
155        assert!(
156            pixels(1.0, 2) > pixels(1.0, 1),
157            "a page is more than a line"
158        );
159    }
160}