Skip to content

Instantly share code, notes, and snippets.

@k3a
Created May 13, 2019 19:19
Show Gist options
  • Save k3a/ef98593a3571b1b399169448327ad333 to your computer and use it in GitHub Desktop.
Save k3a/ef98593a3571b1b399169448327ad333 to your computer and use it in GitHub Desktop.

W3C pointer events

https://www.w3.org/TR/pointerevents/ - A good source of inspiration I discovered during my pointer implementation research. It actually maps well to my platform-specific findings described bellow.

Mouse and touch events have been here for historical and/or compatibility reasons and nowadays it makes more sense to implement more generic Pointer Events which can represent all three main input sources.

Mouse/touch/pointer handling in other frameworks

Touch

Qt

UserWidgetClass::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::TouchBegin: // Beginning of a sequence of touch-screen or track-pad events
    case QEvent::TouchUpdate: // Touch-screen event
    case QEvent::TouchEnd: // End of touch-event sequence
    case QEvent::TouchCancel: // Cancellation of touch-event sequence 
    {
        QTouchEvent *touch = static_cast<QTouchEvent *>(event);
        QList<QTouchEvent::TouchPoint> touchPoints = static_cast<QTouchEvent *>(event)->touchPoints();
        foreach (const QTouchEvent::TouchPoint &touchPoint, touchPoints) {
            switch (touchPoint.state()) {
            case Qt::TouchPointPressed: // The touch point is now pressed.
            case Qt::TouchPointMoved: // The touch point moved.
            case Qt::TouchPointStationary: // The touch point did not move.
            case Qt::TouchPointReleased: // The touch point was released.
                if (touch->device()->capabilities() & QTouchDevice::Pressure)
                    pressure = touchPoint.pressure();
                QPointF positionInWidgetCoords = touchPoint.pos()
                QPointingDeviceUniqueId touchUniqueID = touchPoint.uniqueId();
                qreal pressure = touchPoint.pressure();
                QPointF startPosInTargetWidgetCoords = touchPoint.startPos();
                QPointF startPosInScreenCoords = touchPoint.startScreenPos();
                QPointF startPosInNormalized01ScreenCoords = touchPoint.startNormalizedPos();
                QVector2D vectorPixelsPerSecond = touchPoint.velocity();

                // for QTabletEvent:
                Qt::MouseButton btn = touchEvent.button(); // button which caused the event
                int tiltX = touchEvent.xTilt();
                qreal tangPress = tangentialPressure();
                //... touch attrs - see https://doc.qt.io/qt-5/qtouchevent-touchpoint.html

Android

override fun onTouchEvent(e: MotionEvent): Boolean {
    // handle touch screen motion events

    final int pointerCount = ev.getPointerCount(); // number of pointers (fingers)

    // history of the touch movement - only for "touch move" events!
    final int historySize = ev.getHistorySize(); // num history samples stored
    for (int h = 0; h < historySize; h++) {
        System.out.printf("At time %d:", ev.getHistoricalEventTime(h));
        for (int p = 0; p < pointerCount; p++) {
            System.out.printf("  pointer %d: (%f,%f)",
                ev.getPointerId(p), ev.getHistoricalX(p, h), ev.getHistoricalY(p, h));
        }
     }

    // the most recent touch pos
    System.out.printf("At time %d:", ev.getEventTime());
    for (int p = 0; p < pointerCount; p++) {
        System.out.printf("  pointer %d: (%f,%f)",
            ev.getPointerId(p), ev.getX(p), ev.getY(p));
    }

    when (event.action) {
        MotionEvent.ACTION_DOWN -> print("A pressed gesture has started, the motion contains the initial starting location. A good time to distinguish secondary and tertiary button click")
        MotionEvent.ACTION_MOVE -> print("A change has happened during a press gesture")
        MotionEvent.ACTION_UP -> print("A pressed gesture has finished, the motion contains the final release location")
        MotionEvent.ACTION_CANCEL -> print("The current gesture has been aborted. You will not receive any more points in it. You should treat this as an up event, but not perform any action that you normally would.")
        //...more ACTION_* https://developer.android.com/reference/android/view/MotionEvent#constants_2

    }

    when (event.source) {
        MotionEvent.SOURCE_MOUSE -> print("mouse pointing device")
        MotionEvent.SOURCE_MOUSE_RELATIVE -> print("mouse device whose relative motions should be interpreted as navigation events")
        MotionEvent.SOURCE_TOUCHSCREEN -> print("touch screen pointing device")
        MotionEvent.SOURCE_TOUCHPAD -> print("touch pad or digitizer tablet that is not associated with a display")
        //..more SOURCE_* https://developer.android.com/reference/android/view/InputDevice.html#SOURCE_ANY
    }

    when (event.buttonState) {
        MotionEvent.BUTTON_RIMARY -> print("Primary button (left mouse button). This button constant is not set in response to simple touches with a finger or stylus tip. The user must actually push a button.")
        MouseEvent.BUTTON_SECONDARY -> print("like primary - right mouse button")
        MouseEvent.BUTTON_STYLUS_PRIMARY -> print("differentiates stylus button")
        MouseEvent.BUTTON_BACK -> print("mouse back button")
        //..more https://developer.android.com/reference/android/view/MotionEvent.html#BUTTON_BACK
    }

    //... motion event attrs https://developer.android.com/reference/android/view/MotionEvent

    return true // if event was handled or false otherwise

iOS

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    touch = touches.first

    switch([t phase]) {
        case began, moved, stationary, ended, cancelled: ...
        // https://developer.apple.com/documentation/uikit/uitouch/phase
    }

    float f = touch.force; // force of the touch, where a value of 1.0 represents the force of an average touch (predetermined by the system)
    float maxForce = touch.maximumPossibleForce;
    float altRad = touch.altitudeAngle; // The altitude (=tilt) (in radians) of the stylus
    float aziRad = touch.azimuthAngle; // the azimuth (=orientation) angle (in radians) of the stylus.
    //.. more attrs https://developer.apple.com/documentation/uikit/uitouch#1652981
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {...}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {...}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {...}

Window

static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    POINTER_INFO pi;

    switch (uMsg)
    {
        case WM_POINTERDOWN:
        {
            GetPointerInfo(GET_POINTERID_WPARAM(wParam), &pi);
            break;
        }
        case WM_POINTERUP:
        {
            GetPointerInfo(GET_POINTERID_WPARAM(wParam), &pi);
            break;
        }
        case WM_POINTERUPDATE:
        {
            GetPointerInfo(GET_POINTERID_WPARAM(wParam), &pi);
            break;
        }
    }

    // POINTER_INFO: https://docs.microsoft.com/en-us/windows/desktop/api/winuser/ns-winuser-tagpointer_info
    switch (pi.pointerType) {
        case PT_POINTER: // Generic pointer type. This type never appears in pointer messages
        case PT_TOUCH:
        case PT_PEN:
        case PT_MOUSE:
        case PT_TOUCHPAD:
    }

    uint32_t id = pi.pointerId; // uniquely identifies a pointer during its lifetime. Although if it goes out of detection range and then returns to be detected again, it is treated as a new pointer and may be assigned a new id.

    uint32_t ts = pi.dwTime; // timestamp

    HWND win = pi.hwndTarget; // window targeted by this event

    // POINTER_PEN_INFO for pen https://docs.microsoft.com/en-us/windows/desktop/api/winuser/ns-winuser-tagpointer_pen_info
    POINTER_PEN_INFO penInfo;
    GetPointerPenInfo(pi.pointerId, &penInfo);

    // pen masks used to check for availability of individual structure fields (not all devices support rotation for example)
    if (penInfo.penMask & (PEN_MASK_PRESSURE|PEN_MASK_ROTATION|PEN_MASK_TILT_X|PEN_MASK_TILT_Y)) {
        INT32 tiltX = penInfo.tiltX; // angle of tilt of the pointer along the x-axis in a range of -90 to +90, with a positive value indicating a tilt to the right.

        INT32 tiltY = penInfo.tiltY; // tilt of the pointer along the y-axis in a range of -90 to +90, with a positive value indicating a tilt toward the user.

        UINT32 pressure = penInfo.pressure; // A pen pressure normalized to a range between 0 and 1024. The default is 0 if the device does not report pressure.

        UINT32 rotationDeg = penInfo.rotation; // clockwise rotation, or twist, of the pointer normalized in a range of 0 to 359.
    }

    if (penInfo.penFlags & PEN_FLAG_BARREL) {
        // barrel button is pressed
    }
    if (penInfo.penFlags & PEN_FLAG_INVERTED) {
        // the pen is inverted (probably for the second type of pen, e.g. brush instead of pencil)
    }
    if (penInfo.penFlags & PEN_FLAG_ERASER) {
        // eraser button is pressed
    }

    // POINTER_TOUCH_INFO for touch
    POINTER_TOUCH_INFO touch;
    GetPointerPenInfo(pi.pointerId, &touch);

    // again masks used to check for availability - here require all capabilities
    if (touch.touchMask & (TOUCH_MASK_CONTACTAREA|TOUCH_MASK_ORIENTATION|TOUCH_MASK_PRESSURE) {
        UINT32 press = touch.pressure; // pressure in range between 0 and 1024; 0 default

        // A pointer orientation, with a value between 0 and 359, where 0 indicates a touch pointer aligned with the x-axis and pointing from left to right; increasing values indicate degrees of rotation in the clockwise direction.
        UINT32 orientation = touch.orientation;

        // predicted screen coordinates of the contact area, in pixels. By default, if the device does not report a contact area, this field defaults to a 0-by-0 rectangle centered around the pointer location.
        RECT rect = touch.crContact;

    }
}

Mouse

override fun onTouchEvent(e: MotionEvent): Boolean {
    // basic movements with a mouse button down are like touch events, with event.source set to MotionEvent.SOURCE_MOUSE* and actions ACTION_DOWN/MOVE/UP
}
override fun onGenericMotionEvent (MotionEvent event): Boolean {
    // Generic motion events with source class InputDevice#SOURCE_CLASS_POINTER are delivered to the view under the pointer. All other generic motion events are delivered to the focused view. Hover events are handled specially and are delivered to onHoverEvent(android.view.MotionEvent)
}
override fun onHoverEvent (MotionEvent event): Boolean {
    // This method is called whenever a pointer is hovering into, over, or out of the bounds of a view and the view is not currently being touched.

    // event.source probably still set to distinguish between mouse/touchpad/touchscreen

    when (event.action) {
        MotionEvent.ACTION_HOVER_ENTER -> print("mouse/stylus pointer entered widget")
        MotionEvent.ACTION_HOVER_MOVE -> print("mouse/stylus just hovering over widget, not pressed down")
        MotionEvent.ACTION_HOVER_EXIT -> print("mouse/stylus pointer exited widget")
    }
}

Qt

UserWidgetClass::mousePressEvent(QMouseEvent *event) {...}
UserWidgetClass::mouseMoveEvent(QMouseEvent *event)
{
    // Left, Right, Mid, Extra, etc.. https://doc.qt.io/qt-5/qt.html#MouseButton-enum
    if ((event->buttons() & Qt::LeftButton))
        cursorPositionInWidgetCoords = event->pos();
        shortcutJustXPosInWidgetCoords = event->x();
        cursorPositionInScreenCoords = event->screenPos();
        //... mouse event attrs https://doc.qt.io/qt-5/qmouseevent.html
}
UserWidgetClass::mouseReleaseEvent(QMouseEvent *event) {...}

MacOSX

- (void)mouseEntered:(NSEvent *)theEvent {...} // mouse cursor entered the view bounds
- (void)mouseExited:(NSEvent *)theEvent {...} // mouse cursor exited the view bounds
- (void)mouseDown:(NSEvent *)theEvent {...} // mouse button pressed
- (void)mouseMoved:(NSEvent *)event {
    // pos in the window
    NSPoint locationInWindowThisViewIsIn = [event locationInWindow];

    // needs manual conversion to this view https://developer.apple.com/documentation/appkit/nsview/1483269-convertpoint
    NSPoint locationInThisView = [self convertPoint:locationInWindowThisViewIsIn fromView:nil];
    float xPos = locationInThisView.x; // float :P

    // general event info contains Window associated with the event and timestamp
    // https://developer.apple.com/documentation/appkit/nsevent#1651257
    NSWindow* myWindow = [event window];

    // ...but then there are device/event-specific struct types with various fields:

    // mouse btns https://developer.apple.com/documentation/appkit/nsevent#1651475
    switch ([event buttonNumber]) {
        NSLeftMouse: printf("left")
        NSRightMouse: printf("right")
        8: printf("some special button")
    }
    // or
    NSUInteger mouseButtonMask = [NSEvent pressedMouseButtons];
    BOOL leftMouseButtonDown = (mouseButtonMask & (1 << 0)) != 0;
    BOOL rightMouseButtonDown = (mouseButtonMask & (1 << 1)) != 0;

    // for touch events
    touches = [event allTouches];
    for (NSTouch* t in touches) {
        NSPoint loc = [t location];
        switch([t phase]) {
            case began, moved, stationary, ended, cancelled: ...
            // https://developer.apple.com/documentation/appkit/nstouch/phase
        }

        // https://developer.apple.com/documentation/appkit/nstouch
    }

    // scroll wheel deltas dX, dY, dZ
    float deltaX = [event deltaX];

    // for tablets
    float press = [event pressure]; // the pressure value is embedded in the evennt
    int ser = [event pointingDeviceSerialNumber]; // details about proximity pointing dev

    // for absolute tablets
    int absX = [event absoluteX];
    int absY = [event absoluteY];
    int absZ = [event absoluteZ];

    buttons = [event buttonMask]; // bit mask identifying the buttons pressed for a tablet event

    float rotationDeg = [event rotation]; // if rotation info is available
    NSPoint tilt = [event tilt]; // x, y
}
- (void)mouseDragged:(NSEvent *)theEvent {...} // mouse moved with button pressed
- (void)mouseUp:(NSEvent *)theEvent {...} // mouse button released
- (void)pressureChangeWithEvent:(NSEvent *)event {}; // pressure has changed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment