docs

Mouse and Scroll Input

Forward mouse moves, button clicks, and scroll wheel turns to a View.

On this page

You can forward mouse moves, button clicks, and scroll wheel turns to a View.

A View receives no input on its own— you forward each event, which the page receives as standard DOM mouse or wheel events.

📘 AppCore Handles Input Automatically

AppCore windows (created with Window::Create() after App::Create()) forward mouse, scroll, and keyboard input to their Views automatically— you only need to forward events when you host a View directly.

For key presses and text entry, see Keyboard Input.

Forwarding Mouse Movement

To report cursor motion, create a MouseEvent with type MouseEvent::kType_MouseMoved and pass it to View::FireMouseEvent().

MouseEvent evt;
evt.type = MouseEvent::kType_MouseMoved;
evt.x = 100;
evt.y = 100;
evt.button = MouseEvent::kButton_None;

view->FireMouseEvent(evt);
///
/// Create the event, fire it, then destroy it.
///
ULMouseEvent evt = ulCreateMouseEvent(kMouseEventType_MouseMoved, 100, 100,
                                      kMouseButton_None);

ulViewFireMouseEvent(view, evt);
ulDestroyMouseEvent(evt);

Set button to MouseEvent::kButton_None when reporting movement.

When the cursor leaves the View's area, send a move event with coordinates set to (-1, -1). This clears active CSS hover states and dispatches a mouseleave event to the page.

Forwarding Button Clicks

To send a click, dispatch a MouseEvent::kType_MouseDown event followed by a MouseEvent::kType_MouseUp event at the same position.

C++
MouseEvent evt;
evt.x = 100;
evt.y = 100;
evt.button = MouseEvent::kButton_Left;

evt.type = MouseEvent::kType_MouseDown;
view->FireMouseEvent(evt);

evt.type = MouseEvent::kType_MouseUp;
view->FireMouseEvent(evt);

The button property indicates which button changed state. These constants map directly to DOM MouseEvent.button values on the page:

Button Constant Page button Value
MouseEvent::kButton_Left 0
MouseEvent::kButton_Middle 1
MouseEvent::kButton_Right 2
MouseEvent::kButton_Back 3 (first side button— the library performs no history navigation)
MouseEvent::kButton_Forward 4 (second side button)

The View tracks multi-click timing internally— send every button press as MouseEvent::kType_MouseDown, including the OS's double-click messages.

MouseEvent has no fields for modifier keys— the page reads properties like shiftKey and ctrlKey from the last key event you fired on the View. You should forward modifier key presses and releases as key events to keep these properties accurate (see Keyboard Input).

Context Menus

Forwarding a right-button click (MouseEvent::kButton_Right) dispatches a DOM contextmenu event to the page, which you can handle in JavaScript:

JavaScript
document.addEventListener('contextmenu', (event) => {
  openMenu(event.clientX, event.clientY);  // your own HTML menu
});

Ultralight never displays a menu of its own, so the page doesn't need to cancel the event.

Pressing an unmodified Menu key (KeyCodes::GK_APPS) or Shift+F10 also dispatches contextmenu on the focused element. The View fires the event once per press (auto-repeat is ignored) as long as the page doesn't cancel the keydown event.

This only works when the View receives those key events. If you forward input yourself, you can support both keys. AppCore on Windows doesn't forward system keys— Shift+F10 never reaches the page there.

On Windows, contextmenu fires when you release the right button— on macOS and Linux, it fires as soon as you press it.

Converting Coordinates

To convert window device pixels into View coordinates, subtract the View's offset and divide by View::device_scale().

C++
///
/// Called by our window with the cursor position in device pixels. 'view_x'
/// and 'view_y' are where the View's top-left corner sits, also in device
/// pixels.
///
void OnWindowMouseMove(int x, int y, int view_x, int view_y) {
  double scale = view->device_scale();

  MouseEvent evt;
  evt.type = MouseEvent::kType_MouseMoved;
  evt.x = (int)((x - view_x) / scale);
  evt.y = (int)((y - view_y) / scale);
  evt.button = MouseEvent::kButton_None;

  view->FireMouseEvent(evt);
}

Coordinates passed to MouseEvent use logical pixels relative to the top-left corner of the View (0, 0). For example, on a display with a device scale of 2.0, a cursor at device position (200, 100) maps to logical position (100, 50).

For more on screen coordinates and display scaling, see Windows, Monitors, and DPI.

Forwarding Scroll Events

To forward scroll wheel turns, create a ScrollEvent and pass it to View::FireScrollEvent().

C++
ScrollEvent evt;
evt.type = ScrollEvent::kType_ScrollByPixel;
evt.delta_x = 0;
evt.delta_y = -100;  // one wheel notch, scrolling down

view->FireScrollEvent(evt);

When you use ScrollEvent::kType_ScrollByPixel, specify deltas in logical pixels. A negative delta_y scrolls down and a negative delta_x scrolls right— rolling the wheel toward you produces a positive deltaY on the page, as in a browser.

For a single wheel notch, send about 100 logical pixels. Pages that read the legacy wheelDelta property see 120 for every 100 pixels— this matches one notch in older scripts.

The wheel scrolls whichever element sits under the cursor. Setting x and y specifies that cursor position, or leaving them at -1 applies scrolling to the last cursor position you forwarded.