Monday, December 10, 2012

What's new in XI 2.3 - Pointer Barrier events and barrier releases

Update 7 March 2013: XI 2.3 is released, amend accordingly

The main feature making up XI 2.3 is Pointer Barrier events. This feature was initiated by Chris Halse-Rogers and then taken up by Jasper St. Pierre and me, until it became part of XI 2.3.

The usual conditions apply: a client must announce XI2.3 support with the XIQueryVersion() request to utilise any of the below.

Pointer barriers

Pointer Barriers are an XFixes v5.0 additions to restrict pointer movement. They were first available in server 1.11 (released Aug 2011). A pointer barrier is created by a client along a specific horizontal or vertical line of pixels. Relative input devices such as mice or touchpads will be constrained by this barrier, preventing the cursor from moving across the barrier.

For example, GNOME3 creates a vertical barrier on the top-left screen edge. In a multi-monitor screen, flicking the pointer up to the Activities menu will constrain the pointer there even if there is a screen to the left of the menu. This makes the menu much easier to hit.

The client can choose for the barrier to be transparent in specific directions. GNOME3's pointer barrier is permissive in the direction of positive X (i.e. left-to-right). Thus, moving from the left screen to the right is unrestricted, even though the pointer is constrained when moving in the other direction.

A simple client that creates a vertical pointer barrier looks like this:

Display *dpy = XOpenDisplay(NULL);
int fixes_opcode, fixes_event_base, fixes_error_base;
PointerBarrier barrier;

if (!XQueryExtension(dpy, "XFIXES",
                     &fixes_opcode,
                     &fixes_event_base,
                     &fixes_error_base))
    return EXIT_FAILURE;

/* vertical barrier from 20/20 to 20/100 */
barrier = XFixesCreatePointerBarrier(dpy, DefaultRootWindow(dpy),
                                     20, 20,
                                     20, 100,
                                     0, /* block in all directions */
                                     0, NULL); /* no per-device barriers */

mainloop();

The above code will set up a barrier for all devices, blocking in all directions.

Pointer barrier events

As of XI 2.3, pointer barrier events are sent to clients when pointer movement is constrained by a barrier, provided that client created pointer barriers, the clients have the matching event mask set. Two new event types were added, XI_BarrierHit and XI_BarrierLeave. Both events are only sent to clients owning a barrier and are only sent to the window used to create the barrier (the root window in the example above).

unsigned char m[XIMaskLen(XI_BarrierLeave)] = {0};
XIEventMask mask;
mask.deviceid = XIAllMasterDevices;
mask.mask_len = XIMaskLen(XI_BarrierLeave);
mask.mask = m;

XISetMask(mask.mask, XI_BarrierHit);
XISetMask(mask.mask, XI_BarrierLeave);
XISelectEvents(dpy, DefaultRootWindow(dpy), &mask, 1);
XSync(dpy, False);

while (1) {
    XEvent ev;
    XNextEvent(dpy, &ev);
    if (ev.type != GenericEvent || ev.xcookie.extension != xi2_opcode)
       continue;

    XGetEventData(dpy, &ev.xcookie);
    XIBarrierEvent *b = ev.xcookie.data;
    if (b->evtype == XI_BarrierHit)
       printf("Pointer hit the barrier\n");
    else if (b->evtype == XI_BarrierLeave)
       printf("Pointer left the barrier\n");
    
    XFreeEventData(dpy, &ev.xcookie);
}

An XI_BarrierHit event is first sent when the pointer movement is first constrained by a barrier. It includes some information such as the device, the barrier and the window. It also includes coordinate data.

XI_BarrierHit events are sent for each movement of the pointer against this barrier, or along it, until the pointer finally moves away from the barrier again. "Moving away" means a move to different position that is not blocked by the barrier.

  • if the barrier is vertical, the pointer moves to a different X axis value, or
  • if the barrier is horizontal, the pointer moves to a different Y axis value, or
  • the pointer moves past the barrier's end point
Once the pointer does move away, a XI_BarrierLeave event is sent. A pointer that moves against a barrier, pushes against it for 3 more events and then pulls back will thus generate 4 XI_BarrierHit events and one XI_BarrierLeave event.

Who gets events? Always the client that created the barrier, and only if the window has the event mask set. If the client has a grab on the device with the grab_window being the barrier window, the barrier events follow the usual grab event mask behaviour:

  • if the grab event mask has XI_BarrierHit set, the event is delivered
  • if the grab event mask does not have XI_BarrierHit set but the window mask does and owner_events is True, the event is delivered
  • if owner_events is False and the grab mask does not have XI_BarrierHit set, no event is sent
The above applies to XI_BarrierLeave events as well.

If the client's grab has a grab_window different to the barrier window, or the device is grabbed by another client, event delivery is as usual. In all cases, if the device is grabbed, the XIBarrierDeviceIsGrabbed flag is set. Clients should use this flag to determine what to do. For example, the barrier that is used to trigger the GNOME overlay should probably not trigger if another client has a grab as it may interfere with drag-and-drop.

Coordinates and time in pointer barrier events

Barrier events contain two sets of x/y coordinates. First, the root coordinates which represent the position of the pointer after being confined by barrier (and screen extents, where applicable). This coordinate is the same you would get from a subsequent XIQueryPointer request.

The second set are the delta coordinates (dx/dy), in screen coordinates, from the last pointer position, had the barrier not constrained it. So you can calculate how much the pointer would have moved and thus derive speed. The dtime field in the event helps you to calculate speed, it provides the time in milliseconds since the last pointer event. The deltas are calculated after taking pointer acceleration into account.

    XIBarrierEvent *b = ev.xcookie.data;
    double dx, dy;
    double speed;
    unsigned int millis;

    dx = b->dx;
    dy = b->dy;
    millis = b->dtime;

    speed = sqrt(dx * dx + dy * dy) / millis * 1000;

    printf("Barrier was hit at %.2f/%.2f at %.2f pixels/sec\n",
           b->root_x, b->root_y,  speed);

Releasing a pointer from barrier constraints

By default, a pointer barrier blocks all movement of relative input devices across a barrier. However, a client can opt to temporarily release the pointer from the barrier constraints with the XIBarrierReleasePointer request.

To do so, the client needs the event ID of the barrier event. Since a pointer may bump against the same barrier multiple times before the client reacts (X is asynchronous, after all), the event ID serves to identify a set of movements against or along the pointer barrier.

An event ID is assigned to the first XI_BarrierHit event, and then it remains the same until the XI_BarrierLeave event. This event is the last event with the current event ID, any future barrier events will have a new event ID. This approach may be familiar to you from dealing with touch events, that use a similar approach (touch IDs start at TouchBegin and are tracked through to the TouchEnd).

To release a pointer and let it pass through the barrier, call XIBarrierReleasePointer().

    XIBarrierEvent *b = ev.xcookie.data;
    ...
    if (speed > 200) {
        printf("Movement exceeds speed limit, allowing pointer to go through\n")
        XIBarrierReleasePointer(dpy, b->deviceid, b->barrier, b->eventid);
        XFlush(dpy);
    }
If, when the request arrives at the server, the pointer is still trapped by the barrier, the barrier is now transparent and the pointer can move through it with the next movement. If the pointer moves away from the barrier after releasing it and later moves against this barrier again, it will be constrained once more (albeit with a different eventid). Likewise, if the pointer has already moved away and against the barrier again before the client reacted, the release request has no effect.

If the release does succeed and the pointer moves through the barrier, the client gets a XI_BarrierLeave event with the XIBarrierPointerReleased flag.

The pointer barrier hit-box

As mentioned above, a XI_BarrierLeave event is sent when the pointer moves away from the barrier. This would usually require a 1 pixel movement (let's ignore subpixel-movement, our hand's aren't that precise). However, during testing we found that 1 px pointer-movement can still happen even when a user tries hart to move along the barrier, or even when pushing against the barrier. Thus, we implemented a hit-box of 2 pixels. Thus, a 1 px movement along the barrier still counts as hitting the barrier, and the pointer is not treated as having left the barrier until it leaves the hit-box.

Testing the current state

The current code is available in the barriers branch of the following repositories:

git://people.freedesktop.org/~whot/inputproto.git 
git://people.freedesktop.org/~whot/libXi.git 
git://people.freedesktop.org/~whot/xserver.git 
An example program to test the feature is available here

Tuesday, December 4, 2012

Two magic words - "I think"

We live in a world of absolutes. Water is wet, taxes are annoying, death is certain, it's always beer o'clock somewhere and the world is going to end somewhen later this year (which makes this post doubly futile, but what the hell). Hard to dispute any of those.

We also live in a world of non-absolutes. You may be freezing when someone right next to you is hot. You may be enjoying a movie when the person you dragged into the theatre is wishing for stronger teeth to gnaw through that wrist. Saying "it's cold" or "this movie is awesome" is going to be met with disagreement

What does that have to do with software development? We tend to be so convinced of ourselves that we take our personal opinion or experience as absolutes. It's acceptable to say "it's broken" when the application crashes once you start typing. But to say "it's broken" because it doesn't work as you expected is bad.

Such a statement kills the climate of any conversation. If you tell me that something "is broken", "is completely borked", "is idiotic", or any of those you're achieving one thing: You're putting me on the defence, and quite offensively so. After all, how could I have possibly written, approved, applied something that's so clearly wrong? That makes me uncomfortable. So I'll likely attack back because all I need to do now is find one example that proves you wrong. And that's usually quite easy. But at that point it's unlikely a decent conversation will ensue.

Worse, if you keep doing that at some point I'll stop listening because someone who makes false statements all the time should be in politics or advertising, but not involved with FOSS.

So now let's try the same again with those magic words: "I think this is completely broken". Well, now you've changed it from an absolute statement to an expression of your opinion. So my task is now to convince you to reconsider your opinion. I'm not on the defence, and the result is that in the worst case we'll agree to disagree - without that awkward absolute statement in the room.

A few examples:

  • "Nobody needs this" vs. "I don't think anybody needs this"
  • "The application must not do this" vs "I think the application must not do this"
  • "Wearing underpants is wrong" vs "I think wearing underpants is wrong"
  • "Bananas must be peeled from the bottom" vs "I think bananas must be peeled from the bottom"
Did you notice any differences? Any of the "I think" can be quite simply countered with "I don't think so" (especially the third one, please) and all you'd have is a discussion of two people with differing viewpoints. Even re-reading these examples my reaction to the first is automatically "WTF?", but the reaction to the "I think" alternatives is a conversational "nah, mate".

So next time you're not happy about something: just prefix your criticism with "I think". You may be surprised what difference it makes to the conversation.

Oh, two other magic words: "for me". Compare "This workflow is completely broken" vs "This workflow is completely broken for me". Amazing what difference those two words make...