diff --git a/Cargo.toml b/Cargo.toml index d794e1a6..5c9c7393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ web-sys = { version = "0.3.81", features = [ 'WebGlTexture', 'WebGlUniformLocation', 'WebGlVertexArrayObject', + 'WheelEvent', 'Window', ] } compact_str = "0.9.0" diff --git a/examples/minimal/src/main.rs b/examples/minimal/src/main.rs index 216c3a03..f5af52aa 100644 --- a/examples/minimal/src/main.rs +++ b/examples/minimal/src/main.rs @@ -18,6 +18,7 @@ fn main() -> io::Result<()> { let mouse_position = Rc::new(RefCell::new((0, 0))); let mouse_button = Rc::new(RefCell::new(None::)); let mouse_event_kind = Rc::new(RefCell::new(None::)); + let scroll_count = Rc::new(RefCell::new(0i32)); let terminal = MultiBackendBuilder::with_fallback(BackendType::Dom) .webgl2_options(WebGl2BackendOptions::new() @@ -50,11 +51,28 @@ fn main() -> io::Result<()> { } }); + terminal.on_wheel_event({ + let scroll_count_cloned = scroll_count.clone(); + move |mouse_event| { + let mut scroll_count = scroll_count_cloned.borrow_mut(); + match mouse_event.event { + MouseEventKind::ScrolledVertical(delta) => { + *scroll_count += delta.to_steps(); + } + MouseEventKind::ScrolledHorizontal(delta) => { + *scroll_count += delta.to_steps(); + } + _ => {} + } + } + }); + terminal.draw_web(move |f| { let counter = counter.borrow(); let mouse_position = mouse_position.borrow(); let mouse_button = mouse_button.borrow(); let mouse_event_kind = mouse_event_kind.borrow(); + let scroll_count = scroll_count.borrow(); f.render_widget( Paragraph::new(format!( @@ -62,7 +80,8 @@ fn main() -> io::Result<()> { MouseX: {:?}\n\ MouseY: {:?}\n\ MouseButton: {mouse_button:?}\n\ - MouseEvent: {mouse_event_kind:?}", + MouseEvent: {mouse_event_kind:?}\n\ + Scroll wheel steps: {scroll_count}", mouse_position.0, mouse_position.1 )) .alignment(Alignment::Center) diff --git a/src/event.rs b/src/event.rs index 07f9590e..ca981660 100644 --- a/src/event.rs +++ b/src/event.rs @@ -142,6 +142,40 @@ pub enum MouseButton { Unidentified, } +/// Scroll delta with the original delta mode from the browser. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum ScrollDelta { + /// Delta in pixels + Pixels(i32), + /// Delta in lines (typically represents wheel notches) + Lines(i32), + /// Delta in pages + Pages(i32), +} + +impl ScrollDelta { + /// DOM_DELTA_PIXEL: The units of measurement for the delta are pixels. + const DOM_DELTA_PIXEL: u32 = 0; + /// DOM_DELTA_LINE: The units of measurement for the delta are individual lines of text. + const DOM_DELTA_LINE: u32 = 1; + /// DOM_DELTA_PAGE: The units of measurement for the delta are pages. + const DOM_DELTA_PAGE: u32 = 2; + + /// Normalize the scroll delta to discrete wheel steps/clicks. + /// + /// This converts the delta to an approximate number of wheel notches: + /// - Lines: Already represent wheel notches, returned as-is + /// - Pixels: Divided by ~100 pixels per notch + /// - Pages: Multiplied by ~10 notches per page + pub fn to_steps(self) -> i32 { + match self { + ScrollDelta::Pixels(px) => px / 100, + ScrollDelta::Lines(lines) => lines, + ScrollDelta::Pages(pages) => pages * 10, + } + } +} + /// A mouse event. #[derive(Debug, Clone, Eq, PartialEq)] pub enum MouseEventKind { @@ -151,6 +185,10 @@ pub enum MouseEventKind { Pressed, /// Mouse button released Released, + /// Mouse scrolled vertically (positive = down, negative = up) + ScrolledVertical(ScrollDelta), + /// Mouse scrolled horizontally (positive = right, negative = left) + ScrolledHorizontal(ScrollDelta), /// Unidentified mouse event Unidentified, } @@ -205,3 +243,47 @@ impl From for MouseEventKind { } } } + +/// Convert a [`web_sys::WheelEvent`] to a [`MouseEvent`]. +impl From for MouseEvent { + fn from(event: web_sys::WheelEvent) -> Self { + let ctrl = event.ctrl_key(); + let alt = event.alt_key(); + let shift = event.shift_key(); + let delta_mode = event.delta_mode(); + let delta_x = event.delta_x(); + let delta_y = event.delta_y(); + + // Create ScrollDelta based on the browser's delta mode + let to_scroll_delta = |delta: f64| -> ScrollDelta { + let delta_int = delta as i32; + match delta_mode { + ScrollDelta::DOM_DELTA_PIXEL => ScrollDelta::Pixels(delta_int), + ScrollDelta::DOM_DELTA_LINE => ScrollDelta::Lines(delta_int), + ScrollDelta::DOM_DELTA_PAGE => ScrollDelta::Pages(delta_int), + _ => ScrollDelta::Pixels(delta_int), // fallback to pixels + } + }; + + let scroll_x = to_scroll_delta(delta_x); + let scroll_y = to_scroll_delta(delta_y); + + // Determine the event kind based on which delta is larger + // Compare normalized steps to determine primary scroll direction + let event_kind = if scroll_x.to_steps().abs() > scroll_y.to_steps().abs() { + MouseEventKind::ScrolledHorizontal(scroll_x) + } else { + MouseEventKind::ScrolledVertical(scroll_y) + }; + + MouseEvent { + button: MouseButton::Unidentified, + event: event_kind, + x: event.client_x() as u32, + y: event.client_y() as u32, + ctrl, + alt, + shift, + } + } +} diff --git a/src/render.rs b/src/render.rs index bc559ff8..0697a794 100644 --- a/src/render.rs +++ b/src/render.rs @@ -65,6 +65,24 @@ pub trait WebRenderer { closure.forget(); } + /// Handles wheel events. + /// + /// This method takes a closure that will be called on every `wheel` event. + fn on_wheel_event(&self, mut callback: F) + where + F: FnMut(MouseEvent) + 'static, + { + let closure = Closure::::new(move |event: web_sys::WheelEvent| { + callback(event.into()); + }); + let window = window().unwrap(); + let document = window.document().unwrap(); + document + .add_event_listener_with_callback("wheel", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); + } + /// Requests an animation frame. fn request_animation_frame(f: &Closure) { window()