177 lines
6.7 KiB
Go
177 lines
6.7 KiB
Go
|
/*
|
||
|
Example compress-events shows how to manipulate the xevent package's event
|
||
|
queue to compress events that arrive more often than you'd like to process
|
||
|
them. This example in particular shows how to compress MotionNotify events,
|
||
|
but the same approach could be used to compress ConfigureNotify events.
|
||
|
|
||
|
Note that we show the difference between compressed and uncompressed
|
||
|
MotionNotify events by displaying two windows that listen for MotionNotify
|
||
|
events. The green window compresses them while the red window does not.
|
||
|
Hovering over each window will print the x and y positions in each
|
||
|
MotionNotify event received. You should notice that the red window
|
||
|
lags behind the pointer (particularly if you moved the pointer quickly in
|
||
|
and out of the window) while the green window always keeps up, regardless
|
||
|
of the speed of the pointer.
|
||
|
|
||
|
In each case, we simulate work by sleeping for some amount of time. (The
|
||
|
whole point of compressing events is that there is too much work to be done
|
||
|
for each event.)
|
||
|
|
||
|
Note that when compressing events, you should always make sure that the
|
||
|
event you're compressing *ought* to be compressed. For example, with
|
||
|
MotionNotify events, if the Event field changes, then it applies to a
|
||
|
different window and probably shouldn't be compressed with MotionNotify
|
||
|
events for other windows.
|
||
|
|
||
|
Finally, compressing events implicitly assumes that the event handler doing
|
||
|
the compression is the *only* event handler for a particular (event, window)
|
||
|
tuple. If there is more than one event handler for a single (event, window)
|
||
|
tuple and one of them does compression, the other will be left out in the
|
||
|
cold. (Since the main event loop is subverted and won't process the
|
||
|
compressed events in the usual way.)
|
||
|
|
||
|
N.B. This functionality isn't included in xgbutil because event compression
|
||
|
isn't something that is always desirable, and the conditions under which
|
||
|
compression happens can vary. In particular, compressing ConfigureRequest
|
||
|
events from the perspective of the window manager can be faulty, since
|
||
|
changes to other properties (like WM_NORMAL_HINTS) can change the semantics
|
||
|
of a ConfigureRequest event. (i.e., your compression would need to
|
||
|
specifically look for events that could change future ConfigureRequest
|
||
|
events.)
|
||
|
*/
|
||
|
package main
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"time"
|
||
|
|
||
|
"github.com/jezek/xgb/xproto"
|
||
|
|
||
|
"github.com/jezek/xgbutil"
|
||
|
"github.com/jezek/xgbutil/xevent"
|
||
|
"github.com/jezek/xgbutil/xwindow"
|
||
|
)
|
||
|
|
||
|
// workTime is the amount of time to sleep to simulate "work" in response to
|
||
|
// MotionNotify events. Increasing this will exacerbate the difference
|
||
|
// between the green and red windows. But if you increase it too much,
|
||
|
// the red window starts to *really* lag, and you'll probably have to kill
|
||
|
// the program.
|
||
|
var workTime = 50 * time.Millisecond
|
||
|
|
||
|
// newWindow creates a new window that listens to MotionNotify events with
|
||
|
// the given backgroundcolor.
|
||
|
func newWindow(X *xgbutil.XUtil, color uint32) *xwindow.Window {
|
||
|
win, err := xwindow.Generate(X)
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
err = win.CreateChecked(X.RootWin(), 0, 0, 400, 400,
|
||
|
xproto.CwBackPixel|xproto.CwEventMask,
|
||
|
color, xproto.EventMaskPointerMotion)
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
win.Map()
|
||
|
return win
|
||
|
}
|
||
|
|
||
|
// compressMotionNotify takes a MotionNotify event, and inspects the event
|
||
|
// queue for any future MotionNotify events that can be received without
|
||
|
// blocking. The most recent MotionNotify event is then returned.
|
||
|
// Note that we need to make sure that the Event, Child, Detail, State, Root
|
||
|
// and SameScreen fields are the same to ensure the same window/action is
|
||
|
// generating events. That is, we are only compressing the RootX, RootY,
|
||
|
// EventX and EventY fields.
|
||
|
// This function is not thread safe, since Peek returns a *copy* of the
|
||
|
// event queue---which could be out of date by the time we dequeue events.
|
||
|
func compressMotionNotify(X *xgbutil.XUtil,
|
||
|
ev xevent.MotionNotifyEvent) xevent.MotionNotifyEvent {
|
||
|
|
||
|
// We force a round trip request so that we make sure to read all
|
||
|
// available events.
|
||
|
X.Sync()
|
||
|
xevent.Read(X, false)
|
||
|
|
||
|
// The most recent MotionNotify event that we'll end up returning.
|
||
|
laste := ev
|
||
|
|
||
|
// Look through each event in the queue. If it's an event and it matches
|
||
|
// all the fields in 'ev' that are detailed above, then set it to 'laste'.
|
||
|
// In which case, we'll also dequeue the event, otherwise it will be
|
||
|
// processed twice!
|
||
|
// N.B. If our only goal was to find the most recent relevant MotionNotify
|
||
|
// event, we could traverse the event queue backwards and simply use
|
||
|
// the first MotionNotify we see. However, this could potentially leave
|
||
|
// other MotionNotify events in the queue, which we *don't* want to be
|
||
|
// processed. So we stride along and just pick off MotionNotify events
|
||
|
// until we don't see any more.
|
||
|
for i, ee := range xevent.Peek(X) {
|
||
|
if ee.Err != nil { // This is an error, skip it.
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Use type assertion to make sure this is a MotionNotify event.
|
||
|
if mn, ok := ee.Event.(xproto.MotionNotifyEvent); ok {
|
||
|
// Now make sure all appropriate fields are equivalent.
|
||
|
if ev.Event == mn.Event && ev.Child == mn.Child &&
|
||
|
ev.Detail == mn.Detail && ev.State == mn.State &&
|
||
|
ev.Root == mn.Root && ev.SameScreen == mn.SameScreen {
|
||
|
|
||
|
// Set the most recent/valid motion notify event.
|
||
|
laste = xevent.MotionNotifyEvent{&mn}
|
||
|
|
||
|
// We cheat and use the stack semantics of defer to dequeue
|
||
|
// most recent motion notify events first, so that the indices
|
||
|
// don't become invalid. (If we dequeued oldest first, we'd
|
||
|
// have to account for all future events shifting to the left
|
||
|
// by one.)
|
||
|
defer func(i int) { xevent.DequeueAt(X, i) }(i)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// This isn't strictly necessary, but is correct. We should update
|
||
|
// xgbutil's sense of time with the most recent event processed.
|
||
|
// This is typically done in the main event loop, but since we are
|
||
|
// subverting the main event loop, we should take care of it.
|
||
|
X.TimeSet(laste.Time)
|
||
|
|
||
|
return laste
|
||
|
}
|
||
|
|
||
|
func main() {
|
||
|
X, err := xgbutil.NewConn()
|
||
|
if err != nil {
|
||
|
log.Fatal(err)
|
||
|
}
|
||
|
|
||
|
// Create window for receiving compressed MotionNotify events.
|
||
|
cwin := newWindow(X, 0x00ff00)
|
||
|
|
||
|
// Attach event handler for MotionNotify that compresses events.
|
||
|
xevent.MotionNotifyFun(
|
||
|
func(X *xgbutil.XUtil, ev xevent.MotionNotifyEvent) {
|
||
|
ev = compressMotionNotify(X, ev)
|
||
|
fmt.Printf("COMPRESSED: (EventX %d, EventY %d)\n",
|
||
|
ev.EventX, ev.EventY)
|
||
|
time.Sleep(workTime)
|
||
|
}).Connect(X, cwin.Id)
|
||
|
|
||
|
// Create window for receiving uncompressed MotionNotify events.
|
||
|
uwin := newWindow(X, 0xff0000)
|
||
|
|
||
|
// Attach event handler for MotionNotify that does not compress events.
|
||
|
xevent.MotionNotifyFun(
|
||
|
func(X *xgbutil.XUtil, ev xevent.MotionNotifyEvent) {
|
||
|
fmt.Printf("UNCOMPRESSED: (EventX %d, EventY %d)\n",
|
||
|
ev.EventX, ev.EventY)
|
||
|
time.Sleep(workTime)
|
||
|
}).Connect(X, uwin.Id)
|
||
|
|
||
|
xevent.Main(X)
|
||
|
}
|