aboutsummaryrefslogtreecommitdiff
path: root/prototypes
diff options
context:
space:
mode:
Diffstat (limited to 'prototypes')
-rw-r--r--prototypes/tls-autodetect.go451
-rw-r--r--prototypes/xgb-draw.go329
-rw-r--r--prototypes/xgb-image.go313
-rw-r--r--prototypes/xgb-keys.go213
-rw-r--r--prototypes/xgb-monitors.go40
-rw-r--r--prototypes/xgb-text-viewer.go538
-rw-r--r--prototypes/xgb-window.go156
-rw-r--r--prototypes/xgb-xrender.go397
8 files changed, 2437 insertions, 0 deletions
diff --git a/prototypes/tls-autodetect.go b/prototypes/tls-autodetect.go
new file mode 100644
index 0000000..0427465
--- /dev/null
+++ b/prototypes/tls-autodetect.go
@@ -0,0 +1,451 @@
+//
+// Copyright (c) 2018, Přemysl Janouch <p@janouch.name>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+//
+// This is an example TLS-autodetecting chat server.
+//
+// These clients are unable to properly shutdown the connection on their exit:
+// telnet localhost 1234
+// openssl s_client -connect localhost:1234
+//
+// While this one doesn't react to an EOF from the server:
+// ncat -C localhost 1234
+// ncat -C --ssl localhost 1234
+//
+package main
+
+import (
+ "bufio"
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+)
+
+// --- Utilities ---------------------------------------------------------------
+
+//
+// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom
+// must be at least three octets long for this to work reliably, but that should
+// not pose a problem in practice. We might try waiting for them.
+//
+// SSL2: 1xxx xxxx | xxxx xxxx | <1>
+// (message length) (client hello)
+// SSL3/TLS: <22> | <3> | xxxx xxxx
+// (handshake)| (protocol version)
+//
+func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
+ sysconn.Read(func(fd uintptr) (done bool) {
+ var buf [3]byte
+ n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK)
+ switch {
+ case n == 3:
+ isTLS = buf[0]&0x80 != 0 && buf[2] == 1
+ fallthrough
+ case n == 2:
+ isTLS = isTLS || buf[0] == 22 && buf[1] == 3
+ case n == 1:
+ isTLS = buf[0] == 22
+ case err == syscall.EAGAIN:
+ return false
+ }
+ return true
+ })
+ return isTLS
+}
+
+// --- Declarations ------------------------------------------------------------
+
+type connCloseWriter interface {
+ net.Conn
+ CloseWrite() error
+}
+
+type client struct {
+ transport net.Conn // underlying connection
+ tls *tls.Conn // TLS, if detected
+ conn connCloseWriter // high-level connection
+ inQ []byte // unprocessed input
+ outQ []byte // unprocessed output
+ reading bool // whether a reading goroutine is running
+ writing bool // whether a writing goroutine is running
+ closing bool // whether we're closing the connection
+ killTimer *time.Timer // timeout
+}
+
+type preparedEvent struct {
+ client *client
+ host string // client's hostname or literal IP address
+ isTLS bool // the client seems to use TLS
+}
+
+type readEvent struct {
+ client *client
+ data []byte // new data from the client
+ err error // read error
+}
+
+type writeEvent struct {
+ client *client
+ written int // amount of bytes written
+ err error // write error
+}
+
+var (
+ sigs = make(chan os.Signal, 1)
+ conns = make(chan net.Conn)
+ prepared = make(chan preparedEvent)
+ reads = make(chan readEvent)
+ writes = make(chan writeEvent)
+ timeouts = make(chan *client)
+
+ tlsConf *tls.Config
+ clients = make(map[*client]bool)
+ listener net.Listener
+ inShutdown bool
+ shutdownTimer <-chan time.Time
+)
+
+// --- Server ------------------------------------------------------------------
+
+// Broadcast to all /other/ clients (telnet-friendly, also in accordance to
+// the plan of extending this to an IRCd).
+func broadcast(line string, except *client) {
+ for c := range clients {
+ if c != except {
+ c.send(line)
+ }
+ }
+}
+
+// Initiate a clean shutdown of the whole daemon.
+func initiateShutdown() {
+ log.Println("shutting down")
+ if err := listener.Close(); err != nil {
+ log.Println(err)
+ }
+ for c := range clients {
+ c.closeLink()
+ }
+
+ shutdownTimer = time.After(3 * time.Second)
+ inShutdown = true
+}
+
+// Forcefully tear down all connections.
+func forceShutdown(reason string) {
+ if !inShutdown {
+ log.Fatalln("forceShutdown called without initiateShutdown")
+ }
+
+ log.Printf("forced shutdown (%s)\n", reason)
+ for c := range clients {
+ c.destroy()
+ }
+}
+
+// --- Client ------------------------------------------------------------------
+
+func (c *client) send(line string) {
+ if c.conn != nil && !c.closing {
+ c.outQ = append(c.outQ, (line + "\r\n")...)
+ c.flushOutQ()
+ }
+}
+
+// Tear down the client connection, trying to do so in a graceful manner.
+func (c *client) closeLink() {
+ if c.closing {
+ return
+ }
+ if c.conn == nil {
+ c.destroy()
+ return
+ }
+
+ // Since we send this goodbye, we don't need to call CloseWrite here.
+ c.send("Goodbye")
+ c.killTimer = time.AfterFunc(3*time.Second, func() {
+ timeouts <- c
+ })
+
+ c.closing = true
+}
+
+// Close the connection and forget about the client.
+func (c *client) destroy() {
+ // Try to send a "close notify" alert if the TLS object is ready,
+ // otherwise just tear down the transport.
+ if c.conn != nil {
+ _ = c.conn.Close()
+ } else {
+ _ = c.transport.Close()
+ }
+
+ // Clean up the goroutine, although a spurious event may still be sent.
+ if c.killTimer != nil {
+ c.killTimer.Stop()
+ }
+
+ log.Println("client destroyed")
+ delete(clients, c)
+}
+
+// Handle the results from initializing the client's connection.
+func (c *client) onPrepared(isTLS bool) {
+ if isTLS {
+ c.tls = tls.Server(c.transport, tlsConf)
+ c.conn = c.tls
+ } else {
+ c.conn = c.transport.(connCloseWriter)
+ }
+
+ // TODO: If we've tried to send any data before now, we need to flushOutQ.
+ go read(c)
+ c.reading = true
+}
+
+// Handle the results from trying to read from the client connection.
+func (c *client) onRead(data []byte, readErr error) {
+ if !c.reading {
+ // Abusing the flag to emulate CloseRead and skip over data, see below.
+ return
+ }
+
+ c.inQ = append(c.inQ, data...)
+ for {
+ advance, token, _ := bufio.ScanLines(c.inQ, false /* atEOF */)
+ if advance == 0 {
+ break
+ }
+
+ c.inQ = c.inQ[advance:]
+ line := string(token)
+ fmt.Println(line)
+ broadcast(line, c)
+ }
+
+ if readErr != nil {
+ c.reading = false
+
+ if readErr != io.EOF {
+ log.Println(readErr)
+ c.destroy()
+ } else if c.closing {
+ // Disregarding whether a clean shutdown has happened or not.
+ log.Println("client finished shutdown")
+ c.destroy()
+ } else {
+ log.Println("client EOF")
+ c.closeLink()
+ }
+ } else if len(c.inQ) > 8192 {
+ log.Println("client inQ overrun")
+ // TODO: Inform the client about inQ overrun in the farewell message.
+ c.closeLink()
+
+ // tls.Conn doesn't have the CloseRead method (and it needs to be able
+ // to read from the TCP connection even for writes, so there isn't much
+ // sense in expecting the implementation to do anything useful),
+ // otherwise we'd use it to block incoming packet data.
+ c.reading = false
+ }
+}
+
+// Spawn a goroutine to flush the outQ if possible and necessary.
+func (c *client) flushOutQ() {
+ if !c.writing && c.conn != nil {
+ go write(c, c.outQ)
+ c.writing = true
+ }
+}
+
+// Handle the results from trying to write to the client connection.
+func (c *client) onWrite(written int, writeErr error) {
+ c.outQ = c.outQ[written:]
+ c.writing = false
+
+ if writeErr != nil {
+ log.Println(writeErr)
+ c.destroy()
+ } else if len(c.outQ) > 0 {
+ c.flushOutQ()
+ } else if c.closing {
+ if c.reading {
+ c.conn.CloseWrite()
+ } else {
+ c.destroy()
+ }
+ }
+}
+
+// --- Worker goroutines -------------------------------------------------------
+
+func accept(ln net.Listener) {
+ for {
+ if conn, err := ln.Accept(); err != nil {
+ // TODO: Consider specific cases in error handling, some errors
+ // are transitional while others are fatal.
+ log.Println(err)
+ break
+ } else {
+ conns <- conn
+ }
+ }
+}
+
+func prepare(client *client) {
+ conn := client.transport
+ host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
+ if err != nil {
+ // In effect, we require TCP/UDP, as they have port numbers.
+ log.Fatalln(err)
+ }
+
+ // The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
+ // bothering with pointless contexts.
+ ch := make(chan string, 1)
+ go func() {
+ defer close(ch)
+ if names, err := net.LookupAddr(host); err != nil {
+ log.Println(err)
+ } else {
+ ch <- names[0]
+ }
+ }()
+
+ // While we can't cancel it, we still want to set a timeout on it.
+ select {
+ case <-time.After(5 * time.Second):
+ case resolved, ok := <-ch:
+ if ok {
+ host = resolved
+ }
+ }
+
+ // Note that in this demo application the autodetection prevents non-TLS
+ // clients from receiving any messages until they send something.
+ isTLS := false
+ if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
+ // This is just for the TLS detection and doesn't need to be fatal.
+ log.Println(err)
+ } else {
+ isTLS = detectTLS(sysconn)
+ }
+
+ // FIXME: When the client sends no data, we still initialize its conn.
+ prepared <- preparedEvent{client, host, isTLS}
+}
+
+func read(client *client) {
+ // A new buffer is allocated each time we receive some bytes, because of
+ // thread-safety. Therefore the buffer shouldn't be too large, or we'd
+ // need to copy it each time into a precisely sized new buffer.
+ var err error
+ for err == nil {
+ var (
+ buf [512]byte
+ n int
+ )
+ n, err = client.conn.Read(buf[:])
+ reads <- readEvent{client, buf[:n], err}
+ }
+}
+
+// Flush outQ, which is passed by parameter so that there are no data races.
+func write(client *client, data []byte) {
+ // We just write as much as we can, the main goroutine does the looping.
+ n, err := client.conn.Write(data)
+ writes <- writeEvent{client, n, err}
+}
+
+// --- Main --------------------------------------------------------------------
+
+func processOneEvent() {
+ select {
+ case <-sigs:
+ if inShutdown {
+ forceShutdown("requested by user")
+ } else {
+ initiateShutdown()
+ }
+
+ case <-shutdownTimer:
+ forceShutdown("timeout")
+
+ case conn := <-conns:
+ log.Println("accepted client connection")
+ c := &client{transport: conn}
+ clients[c] = true
+ go prepare(c)
+
+ case ev := <-prepared:
+ log.Println("client is ready, resolved to", ev.host)
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onPrepared(ev.isTLS)
+ }
+
+ case ev := <-reads:
+ log.Println("received data from client")
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onRead(ev.data, ev.err)
+ }
+
+ case ev := <-writes:
+ log.Println("sent data to client")
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onWrite(ev.written, ev.err)
+ }
+
+ case c := <-timeouts:
+ if _, ok := clients[c]; ok {
+ log.Println("client timeouted")
+ c.destroy()
+ }
+ }
+}
+
+func main() {
+ // Just deal with unexpected flags, we don't use any ourselves.
+ flag.Parse()
+
+ if len(flag.Args()) != 3 {
+ log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
+ }
+
+ cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0))
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ tlsConf = &tls.Config{Certificates: []tls.Certificate{cert}}
+ listener, err = net.Listen("tcp", flag.Arg(2))
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ go accept(listener)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ for !inShutdown || len(clients) > 0 {
+ processOneEvent()
+ }
+}
diff --git a/prototypes/xgb-draw.go b/prototypes/xgb-draw.go
new file mode 100644
index 0000000..b893f6f
--- /dev/null
+++ b/prototypes/xgb-draw.go
@@ -0,0 +1,329 @@
+// Network-friendly drawing application based on XRender.
+//
+// TODO: Maybe keep the pixmap as large as the window.
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/render"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+)
+
+func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
+func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
+
+func findPictureFormat(formats []render.Pictforminfo,
+ depth byte, direct render.Directformat) render.Pictformat {
+ for _, pf := range formats {
+ if pf.Depth == depth && pf.Direct == direct {
+ return pf.Id
+ }
+ }
+ return 0
+}
+
+func createNewPicture(X *xgb.Conn, depth byte, drawable xproto.Drawable,
+ width uint16, height uint16, format render.Pictformat) render.Picture {
+ pixmapid, err := xproto.NewPixmapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = xproto.CreatePixmap(X, depth, pixmapid, drawable, width, height)
+
+ pictid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = render.CreatePicture(X, pictid, xproto.Drawable(pixmapid), format,
+ 0, []uint32{})
+ return pictid
+}
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if err := render.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ visual, depth := screen.RootVisual, screen.RootDepth
+ if depth < 24 {
+ log.Fatalln("need more colors")
+ }
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBackPixel|xproto.CwEventMask,
+ []uint32{0xffffffff, xproto.EventMaskButtonPress |
+ xproto.EventMaskButtonMotion | xproto.EventMaskButtonRelease |
+ xproto.EventMaskStructureNotify | xproto.EventMaskExposure})
+
+ title := []byte("Draw")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Find appropriate picture formats.
+ var pformat, pformatAlpha, pformatRGB render.Pictformat
+ for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
+ for _, pv := range pd.Visuals {
+ if pv.Visual == visual {
+ pformat = pv.Format
+ }
+ }
+ }
+ if pformatAlpha = findPictureFormat(pformats.Formats, 8,
+ render.Directformat{
+ AlphaShift: 0,
+ AlphaMask: 0xff,
+ }); pformat == 0 {
+ log.Fatalln("required picture format not found")
+ }
+ if pformatRGB = findPictureFormat(pformats.Formats, 24,
+ render.Directformat{
+ RedShift: 16,
+ RedMask: 0xff,
+ GreenShift: 8,
+ GreenMask: 0xff,
+ BlueShift: 0,
+ BlueMask: 0xff,
+ }); pformatRGB == 0 {
+ log.Fatalln("required picture format not found")
+ }
+
+ // Picture for the window.
+ pid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
+
+ // Brush shape.
+ const brushRadius = 5
+
+ brushid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ cFull := render.Color{0xffff, 0xffff, 0xffff, 0xffff}
+ cTrans := render.Color{0xffff, 0xffff, 0xffff, 0}
+ _ = render.CreateRadialGradient(X, brushid,
+ render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)},
+ render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)},
+ F64ToFixed(0),
+ F64ToFixed(brushRadius),
+ 3, []render.Fixed{F64ToFixed(0), F64ToFixed(0.1), F64ToFixed(1)},
+ []render.Color{cFull, cFull, cTrans})
+
+ // Brush color.
+ colorid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = render.CreateSolidFill(X, colorid, render.Color{
+ Red: 0x4444,
+ Green: 0x8888,
+ Blue: 0xffff,
+ Alpha: 0xffff,
+ })
+
+ // Various pixmaps.
+ const (
+ pixWidth = 1000
+ pixHeight = 1000
+ )
+
+ canvasid := createNewPicture(X, 24,
+ xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB)
+ bufferid := createNewPicture(X, 24,
+ xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB)
+ maskid := createNewPicture(X, 8,
+ xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatAlpha)
+
+ // Smoothing by way of blur, apparently a misguided idea.
+ /*
+ _ = render.SetPictureFilter(X, maskid,
+ uint16(len("convolution")), "convolution",
+ []render.Fixed{F64ToFixed(3), F64ToFixed(3),
+ F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0),
+ F64ToFixed(0.15), F64ToFixed(0.40), F64ToFixed(0.15),
+ F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0)})
+ */
+
+ // Pixmaps come uninitialized.
+ _ = render.FillRectangles(X,
+ render.PictOpSrc, canvasid, render.Color{
+ Red: 0xffff, Green: 0xffff, Blue: 0xffff, Alpha: 0xffff,
+ }, []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}})
+
+ // This is the only method we can use to render brush strokes without
+ // alpha accumulation due to stamping. Though this also seems to be
+ // misguided. Keeping it here for educational purposes.
+ //
+ // ConjointOver is defined as: A = Aa * 1 + Ab * max(1-Aa/Ab,0)
+ // which basically resolves to: A = max(Aa, Ab)
+ // which equals "lighten" with one channel only.
+ //
+ // Resources:
+ // - https://www.cairographics.org/operators/
+ // - http://ssp.impulsetrain.com/porterduff.html
+ // - https://keithp.com/~keithp/talks/renderproblems/renderproblems/render-title.html
+ // - https://keithp.com/~keithp/talks/cairo2003.pdf
+ drawPointAt := func(x, y int16) {
+ _ = render.Composite(X, render.PictOpConjointOver,
+ brushid, render.PictureNone, maskid,
+ 0, 0, 0, 0, x-brushRadius, y-brushRadius,
+ brushRadius*2, brushRadius*2)
+
+ _ = render.SetPictureClipRectangles(X, bufferid,
+ x-brushRadius, y-brushRadius, []xproto.Rectangle{
+ {Width: brushRadius * 2, Height: brushRadius * 2}})
+ _ = render.Composite(X, render.PictOpSrc,
+ canvasid, render.PictureNone, bufferid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+ _ = render.Composite(X, render.PictOpOver,
+ colorid, maskid, bufferid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+
+ // Composited, now blit to window without flicker.
+ _ = render.SetPictureClipRectangles(X, pid,
+ x-brushRadius, y-brushRadius, []xproto.Rectangle{
+ {Width: brushRadius * 2, Height: brushRadius * 2}})
+ _ = render.Composite(X, render.PictOpSrc,
+ bufferid, render.PictureNone, pid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+ }
+
+ // Integer version of Bresenham's line drawing algorithm
+ drawLine := func(x0, y0, x1, y1 int16) {
+ dx, dy := x1-x0, y1-y0
+ if dx < 0 {
+ dx = -dx
+ }
+ if dy < 0 {
+ dy = -dy
+ }
+
+ steep := dx < dy
+ if steep {
+ // Flip the coordinate system on input
+ x0, y0 = y0, x0
+ x1, y1 = y1, x1
+ dx, dy = dy, dx
+ }
+
+ var stepX, stepY int16 = 1, 1
+ if x0 > x1 {
+ stepX = -1
+ }
+ if y0 > y1 {
+ stepY = -1
+ }
+
+ dpr := dy * 2
+ delta := dpr - dx
+ dpru := delta - dx
+
+ for ; dx > 0; dx-- {
+ // Unflip the coordinate system on output
+ if steep {
+ drawPointAt(y0, x0)
+ } else {
+ drawPointAt(x0, y0)
+ }
+
+ x0 += stepX
+ if delta > 0 {
+ y0 += stepY
+ delta += dpru
+ } else {
+ delta += dpr
+ }
+ }
+ }
+
+ var startX, startY int16 = 0, 0
+ drawing := false
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ExposeEvent:
+ _ = render.SetPictureClipRectangles(X, pid, int16(e.X), int16(e.Y),
+ []xproto.Rectangle{{Width: e.Width, Height: e.Height}})
+
+ // Not bothering to deflicker here using the buffer pixmap,
+ // with compositing this event is rare enough.
+ _ = render.Composite(X, render.PictOpSrc,
+ canvasid, render.PictureNone, pid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+
+ if drawing {
+ _ = render.Composite(X, render.PictOpOver,
+ colorid, maskid, pid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+ }
+
+ case xproto.ButtonPressEvent:
+ if e.Detail == xproto.ButtonIndex1 {
+ render.FillRectangles(X,
+ render.PictOpSrc, maskid, render.Color{},
+ []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}})
+
+ drawing = true
+ drawPointAt(e.EventX, e.EventY)
+ startX, startY = e.EventX, e.EventY
+ }
+
+ case xproto.MotionNotifyEvent:
+ if drawing {
+ drawLine(startX, startY, e.EventX, e.EventY)
+ startX, startY = e.EventX, e.EventY
+ }
+
+ case xproto.ButtonReleaseEvent:
+ if e.Detail == xproto.ButtonIndex1 {
+ _ = render.Composite(X, render.PictOpOver,
+ colorid, maskid, canvasid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ pixWidth, pixHeight)
+
+ drawing = false
+ }
+ }
+ }
+}
diff --git a/prototypes/xgb-image.go b/prototypes/xgb-image.go
new file mode 100644
index 0000000..2da37d6
--- /dev/null
+++ b/prototypes/xgb-image.go
@@ -0,0 +1,313 @@
+package main
+
+import (
+ "encoding/binary"
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/render"
+ "github.com/BurntSushi/xgb/shm"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+ "os"
+ "reflect"
+ "time"
+ "unsafe"
+
+ "image"
+ _ "image/gif"
+ _ "image/jpeg"
+ _ "image/png"
+)
+
+// #include <sys/ipc.h>
+// #include <sys/shm.h>
+import "C"
+
+func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
+func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
+
+func main() {
+ /*
+ pf, err := os.Create("pprof.out")
+ if err != nil {
+ log.Fatal(err)
+ }
+ pprof.StartCPUProfile(pf)
+ defer pprof.StopCPUProfile()
+ */
+
+ // Load a picture from the command line.
+ f, err := os.Open(os.Args[1])
+ if err != nil {
+ log.Fatalln(err)
+ }
+ defer f.Close()
+
+ img, name, err := image.Decode(f)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ log.Println("image type is", name)
+
+ // Miscellaneous X11 initialization.
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := render.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ visual, depth := screen.RootVisual, screen.RootDepth
+ // TODO: We should check that we find it, though we don't /need/ alpha here,
+ // it's just a minor improvement--affects the backpixel value.
+ for _, i := range screen.AllowedDepths {
+ for _, v := range i.Visuals {
+ // TODO: Could/should check other parameters.
+ if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor {
+ visual, depth = v.VisualId, i.Depth
+ break
+ }
+ }
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask|
+ xproto.CwColormap, []uint32{0x80808080, 0,
+ xproto.EventMaskStructureNotify | xproto.EventMaskExposure,
+ uint32(mid)})
+
+ title := []byte("Image")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Similar to XRenderFindVisualFormat.
+ // The DefaultScreen is almost certain to be zero.
+ var pformat render.Pictformat
+ for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
+ // This check seems to be slightly extraneous.
+ if pd.Depth != depth {
+ continue
+ }
+ for _, pv := range pd.Visuals {
+ if pv.Visual == visual {
+ pformat = pv.Format
+ }
+ }
+ }
+
+ // Wrap the window's surface in a picture.
+ pid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
+
+ // setup.BitmapFormatScanline{Pad,Unit} and setup.BitmapFormatBitOrder
+ // don't interest us here since we're only using Z format pixmaps.
+ for _, pf := range setup.PixmapFormats {
+ if pf.Depth == 32 {
+ if pf.BitsPerPixel != 32 || pf.ScanlinePad != 32 {
+ log.Fatalln("unsuported X server")
+ }
+ }
+ }
+
+ pixid, err := xproto.NewPixmapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = xproto.CreatePixmap(X, 32, pixid, xproto.Drawable(screen.Root),
+ uint16(img.Bounds().Dx()), uint16(img.Bounds().Dy()))
+
+ var bgraFormat render.Pictformat
+ wanted := render.Directformat{
+ RedShift: 16,
+ RedMask: 0xff,
+ GreenShift: 8,
+ GreenMask: 0xff,
+ BlueShift: 0,
+ BlueMask: 0xff,
+ AlphaShift: 24,
+ AlphaMask: 0xff,
+ }
+ for _, pf := range pformats.Formats {
+ if pf.Depth == 32 && pf.Direct == wanted {
+ bgraFormat = pf.Id
+ break
+ }
+ }
+ if bgraFormat == 0 {
+ log.Fatalln("ARGB format not found")
+ }
+
+ // We could also look for the inverse pictformat.
+ var encoding binary.ByteOrder
+ if setup.ImageByteOrder == xproto.ImageOrderMSBFirst {
+ encoding = binary.BigEndian
+ } else {
+ encoding = binary.LittleEndian
+ }
+
+ pixpicid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ render.CreatePicture(X, pixpicid, xproto.Drawable(pixid), bgraFormat,
+ 0, []uint32{})
+
+ // Do we really need this? :/
+ cid, err := xproto.NewGcontextId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = xproto.CreateGC(X, cid, xproto.Drawable(pixid),
+ xproto.GcGraphicsExposures, []uint32{0})
+
+ bounds := img.Bounds()
+ Lstart := time.Now()
+
+ if err := shm.Init(X); err != nil {
+ log.Println("MIT-SHM unavailable")
+
+ // We're being lazy and resolve the 1<<16 limit of requests by sending
+ // a row at a time. The encoding is also done inefficiently.
+ // Also see xgbutil/xgraphics/xsurface.go.
+ row := make([]byte, bounds.Dx()*4)
+ for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
+ r, g, b, a := img.At(x, y).RGBA()
+ encoding.PutUint32(row[x*4:],
+ (a>>8)<<24|(r>>8)<<16|(g>>8)<<8|(b>>8))
+ }
+ _ = xproto.PutImage(X, xproto.ImageFormatZPixmap,
+ xproto.Drawable(pixid), cid, uint16(bounds.Dx()), 1,
+ 0, int16(y),
+ 0, 32, row)
+ }
+ } else {
+ rep, err := shm.QueryVersion(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if rep.PixmapFormat != xproto.ImageFormatZPixmap ||
+ !rep.SharedPixmaps {
+ log.Fatalln("MIT-SHM configuration unfit")
+ }
+
+ shmSize := bounds.Dx() * bounds.Dy() * 4
+
+ // As a side note, to clean up unreferenced segments (orphans):
+ // ipcs -m | awk '$6 == "0" { print $2 }' | xargs ipcrm shm
+ shmID := int(C.shmget(C.IPC_PRIVATE,
+ C.size_t(shmSize), C.IPC_CREAT|0777))
+ if shmID == -1 {
+ // TODO: We should handle this case by falling back to PutImage,
+ // if only because the allocation may hit a system limit.
+ log.Fatalln("memory allocation failed")
+ }
+
+ dataRaw := C.shmat(C.int(shmID), nil, 0)
+ defer C.shmdt(dataRaw)
+ defer C.shmctl(C.int(shmID), C.IPC_RMID, nil)
+
+ data := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
+ Data: uintptr(dataRaw), Len: shmSize, Cap: shmSize}))
+ for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
+ row := data[y*bounds.Dx()*4:]
+ for x := bounds.Min.X; x < bounds.Max.X; x++ {
+ r, g, b, a := img.At(x, y).RGBA()
+ encoding.PutUint32(row[x*4:],
+ (a>>8)<<24|(r>>8)<<16|(g>>8)<<8|(b>>8))
+ }
+ }
+
+ segid, err := shm.NewSegId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Need to have it attached on the server before we unload the segment.
+ c := shm.AttachChecked(X, segid, uint32(shmID), true /* RO */)
+ if err := c.Check(); err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = shm.PutImage(X, xproto.Drawable(pixid), cid,
+ uint16(bounds.Dx()), uint16(bounds.Dy()), 0, 0,
+ uint16(bounds.Dx()), uint16(bounds.Dy()), 0, 0,
+ 32, xproto.ImageFormatZPixmap,
+ 0 /* SendEvent */, segid, 0 /* Offset */)
+ }
+
+ log.Println("uploading took", time.Now().Sub(Lstart))
+
+ var scale float64 = 1
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ConfigureNotifyEvent:
+ w, h := e.Width, e.Height
+
+ scaleX := float64(bounds.Dx()) / float64(w)
+ scaleY := float64(bounds.Dy()) / float64(h)
+
+ if scaleX < scaleY {
+ scale = scaleY
+ } else {
+ scale = scaleX
+ }
+
+ _ = render.SetPictureTransform(X, pixpicid, render.Transform{
+ F64ToFixed(scale), F64ToFixed(0), F64ToFixed(0),
+ F64ToFixed(0), F64ToFixed(scale), F64ToFixed(0),
+ F64ToFixed(0), F64ToFixed(0), F64ToFixed(1),
+ })
+ _ = render.SetPictureFilter(X, pixpicid, 8, "bilinear", nil)
+
+ case xproto.ExposeEvent:
+ _ = render.Composite(X, render.PictOpSrc,
+ pixpicid, render.PictureNone, pid,
+ 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
+ uint16(float64(img.Bounds().Dx())/scale),
+ uint16(float64(img.Bounds().Dy())/scale))
+ }
+ }
+}
diff --git a/prototypes/xgb-keys.go b/prototypes/xgb-keys.go
new file mode 100644
index 0000000..578ab39
--- /dev/null
+++ b/prototypes/xgb-keys.go
@@ -0,0 +1,213 @@
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ //"github.com/BurntSushi/xgb/xkb"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+)
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ /*
+ // Use the extension if available, makes better use of state bits.
+ if err := xkb.Init(X); err == nil {
+ if _, err := xkb.UseExtension(X, 1, 0).Reply(); err != nil {
+ log.Fatalln(err)
+ }
+ }
+ */
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ visual, depth := screen.RootVisual, screen.RootDepth
+ // TODO: We should check that we find it, though we don't /need/ alpha here,
+ // it's just a minor improvement--affects the backpixel value.
+ for _, i := range screen.AllowedDepths {
+ for _, v := range i.Visuals {
+ // TODO: Could/should check other parameters.
+ if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor {
+ visual, depth = v.VisualId, i.Depth
+ break
+ }
+ }
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask|
+ xproto.CwColormap, []uint32{0x80808080, 0,
+ xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
+ /* KeymapNotify */ xproto.EventMaskKeymapState, uint32(mid)})
+
+ title := []byte("Keys")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ mapping, err := xproto.GetKeyboardMapping(X, setup.MinKeycode,
+ byte(setup.MaxKeycode-setup.MinKeycode+1)).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // The order is "Shift, Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5."
+ mm, err := xproto.GetModifierMapping(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // XXX: This seems pointless, the key will just end up switching groups
+ // instead of levels without full XKB handling. Though perhaps it might
+ // at least work as intended when there's only one XKB group.
+ const MODE_SWITCH = 0xff7e
+
+ var modeSwitchMask uint16
+ for mod := 0; mod < 8; mod++ {
+ perMod := int(mm.KeycodesPerModifier)
+ for _, kc := range mm.Keycodes[mod*perMod : (mod+1)*perMod] {
+ if kc == 0 {
+ continue
+ }
+
+ perKc := int(mapping.KeysymsPerKeycode)
+ k := int(kc - setup.MinKeycode)
+ for _, ks := range mapping.Keysyms[k*perKc : (k+1)*perKc] {
+ if ks == MODE_SWITCH {
+ modeSwitchMask |= 1 << uint(mod)
+ }
+ }
+ }
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.KeymapNotifyEvent:
+ // e.Keys is a 32 * 8 large bitmap indicating which keys are
+ // currently pressed down. This is sent "after every EnterNotify
+ // and FocusIn" but it also seems to fire when the keyboard layout
+ // changes. aixterm manual even speaks of that explicitly.
+ //
+ // But since changing the effective group involves no changes to
+ // the compatibility mapping, there's nothing for us to do.
+
+ case xproto.MappingNotifyEvent:
+ // e.FirstKeyCode .. e.Count changes have happened but rereading
+ // everything is the simpler thing to do.
+ mapping, err = xproto.GetKeyboardMapping(X, setup.MinKeycode,
+ byte(setup.MaxKeycode-setup.MinKeycode+1)).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // TODO: We should also repeat the search for MODE SWITCH.
+
+ case xproto.KeyPressEvent:
+ step := int(mapping.KeysymsPerKeycode)
+ from := int(e.Detail-setup.MinKeycode) * step
+ ks := mapping.Keysyms[from : from+step]
+
+ // Strip trailing NoSymbol entries.
+ for len(ks) > 0 && ks[len(ks)-1] == 0 {
+ ks = ks[:len(ks)-1]
+ }
+
+ // Expand back to at least 4.
+ switch {
+ case len(ks) == 1:
+ ks = append(ks, 0, ks[0], 0)
+ case len(ks) == 2:
+ ks = append(ks, ks[0], ks[1])
+ case len(ks) == 3:
+ ks = append(ks, 0)
+ }
+
+ // Other silly expansion rules, only applied to basic ASCII.
+ if ks[1] == 0 {
+ ks[1] = ks[0]
+ if ks[0] >= 'A' && ks[0] <= 'Z' ||
+ ks[0] >= 'a' && ks[0] <= 'z' {
+ ks[0] = ks[0] | 32
+ ks[1] = ks[0] &^ 32
+ }
+ }
+
+ if ks[3] == 0 {
+ ks[3] = ks[2]
+ if ks[2] >= 'A' && ks[2] <= 'Z' ||
+ ks[2] >= 'a' && ks[2] <= 'z' {
+ ks[2] = ks[2] | 32
+ ks[3] = ks[2] &^ 32
+ }
+ }
+
+ // We only have enough information to switch between two groups.
+ offset := 0
+ if e.State&modeSwitchMask != 0 {
+ offset += 2
+ }
+
+ var result xproto.Keysym
+
+ shift := e.State&xproto.ModMaskShift != 0
+ lock := e.State&xproto.ModMaskLock != 0
+ switch {
+ case !shift && !lock:
+ result = ks[offset+0]
+ case !shift && lock:
+ if ks[offset+0] >= 'a' && ks[offset+0] <= 'z' {
+ result = ks[offset+1]
+ } else {
+ result = ks[offset+0]
+ }
+ case shift && lock:
+ if ks[offset+1] >= 'a' && ks[offset+1] <= 'z' {
+ result = ks[offset+1] &^ 32
+ } else {
+ result = ks[offset+1]
+ }
+ case shift:
+ result = ks[offset+1]
+ }
+
+ if result <= 0xff {
+ log.Printf("%c (Latin-1)\n", rune(result))
+ } else {
+ log.Println(result)
+ }
+ }
+ }
+}
diff --git a/prototypes/xgb-monitors.go b/prototypes/xgb-monitors.go
new file mode 100644
index 0000000..5ce5af9
--- /dev/null
+++ b/prototypes/xgb-monitors.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+
+ // Needs a patched local version with xcb-proto 1.12 and this fix:
+ // -size := xgb.Pad((8 + (24 + xgb.Pad((int(NOutput) * 4)))))
+ // +size := xgb.Pad((8 + (24 + xgb.Pad((int(Monitorinfo.NOutput) * 4)))))
+ "github.com/BurntSushi/xgb/randr"
+)
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := randr.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ ms, err := randr.GetMonitors(X, screen.Root, true /* GetActive */).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ for _, m := range ms.Monitors {
+ reply, err := xproto.GetAtomName(X, m.Name).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ log.Printf("Monitor %s %+v\n", reply.Name, m)
+ }
+}
diff --git a/prototypes/xgb-text-viewer.go b/prototypes/xgb-text-viewer.go
new file mode 100644
index 0000000..e740138
--- /dev/null
+++ b/prototypes/xgb-text-viewer.go
@@ -0,0 +1,538 @@
+// This is an amalgamation of xgb-xrender.go and xgb-keys.go and more of a demo,
+// some comments have been stripped.
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/render"
+ "github.com/BurntSushi/xgb/xproto"
+ "github.com/golang/freetype"
+ "github.com/golang/freetype/truetype"
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/math/fixed"
+ "image"
+ "image/draw"
+ "io/ioutil"
+ "log"
+ "os"
+ "strings"
+)
+
+func glyphListBytes(buf []byte, runes []rune, size int) int {
+ b := 0
+ for _, r := range runes {
+ switch size {
+ default:
+ buf[b] = byte(r)
+ b += 1
+ case 2:
+ xgb.Put16(buf[b:], uint16(r))
+ b += 2
+ case 4:
+ xgb.Put32(buf[b:], uint32(r))
+ b += 4
+ }
+ }
+ return xgb.Pad(b)
+}
+
+// When the len is 255, a GLYPHABLE follows, otherwise a list of CARD8/16/32.
+func glyphEltHeaderBytes(buf []byte, len byte, deltaX, deltaY int16) int {
+ b := 0
+ buf[b] = len
+ b += 4
+ xgb.Put16(buf[b:], uint16(deltaX))
+ b += 2
+ xgb.Put16(buf[b:], uint16(deltaY))
+ b += 2
+ return xgb.Pad(b)
+}
+
+type xgbCookie interface{ Check() error }
+
+// compositeString makes an appropriate render.CompositeGlyphs request,
+// assuming that glyphs equal Unicode codepoints.
+func compositeString(c *xgb.Conn, op byte, src, dst render.Picture,
+ maskFormat render.Pictformat, glyphset render.Glyphset, srcX, srcY int16,
+ destX, destY int16, text string) xgbCookie {
+ runes := []rune(text)
+
+ var highest rune
+ for _, r := range runes {
+ if r > highest {
+ highest = r
+ }
+ }
+
+ size := 1
+ switch {
+ case highest > 1<<16:
+ size = 4
+ case highest > 1<<8:
+ size = 2
+ }
+
+ // They gave up on the XCB protocol API and we need to serialize explicitly.
+
+ // To spare us from caring about the padding, use the largest number lesser
+ // than 255 that is divisible by 4 (for size 2 and 4 the requirements are
+ // less strict but this works in the general case).
+ const maxPerChunk = 252
+
+ buf := make([]byte, (len(runes)+maxPerChunk-1)/maxPerChunk*8+len(runes)*size)
+ b := 0
+
+ for len(runes) > maxPerChunk {
+ b += glyphEltHeaderBytes(buf[b:], maxPerChunk, 0, 0)
+ b += glyphListBytes(buf[b:], runes[:maxPerChunk], size)
+ runes = runes[maxPerChunk:]
+ }
+ if len(runes) > 0 {
+ b += glyphEltHeaderBytes(buf[b:], byte(len(runes)), destX, destY)
+ b += glyphListBytes(buf[b:], runes, size)
+ }
+
+ switch size {
+ default:
+ return render.CompositeGlyphs8(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ case 2:
+ return render.CompositeGlyphs16(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ case 4:
+ return render.CompositeGlyphs32(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ }
+}
+
+type textRenderer struct {
+ f *truetype.Font
+ opts *truetype.Options
+ face font.Face
+
+ bounds fixed.Rectangle26_6 // outer bounds for all the font's glyph
+ buf *image.RGBA // rendering buffer
+
+ X *xgb.Conn
+ gsid render.Glyphset
+ loaded map[rune]bool
+}
+
+func newTextRenderer(X *xgb.Conn, ttf []byte, opts *truetype.Options) (
+ *textRenderer, error) {
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ return nil, err
+ }
+
+ // We use RGBA here just so that lines are padded to 32 bits.
+ // Since there's no subpixel antialiasing and alpha is premultiplied,
+ // it doesn't even mater that RGBA is interpreted as ARGB or BGRA.
+ var rgbFormat render.Pictformat
+ for _, pf := range pformats.Formats {
+ if pf.Depth == 32 && pf.Direct.AlphaMask != 0 {
+ rgbFormat = pf.Id
+ break
+ }
+ }
+
+ tr := &textRenderer{opts: opts, X: X, loaded: make(map[rune]bool)}
+ if tr.f, err = freetype.ParseFont(goregular.TTF); err != nil {
+ return nil, err
+ }
+
+ tr.face = truetype.NewFace(tr.f, opts)
+ tr.bounds = tr.f.Bounds(fixed.Int26_6(opts.Size * float64(opts.DPI) *
+ (64.0 / 72.0)))
+
+ if tr.gsid, err = render.NewGlyphsetId(X); err != nil {
+ return nil, err
+ }
+ if err := render.CreateGlyphSetChecked(X, tr.gsid, rgbFormat).
+ Check(); err != nil {
+ return nil, err
+ }
+
+ tr.buf = image.NewRGBA(image.Rect(
+ +tr.bounds.Min.X.Floor(),
+ -tr.bounds.Min.Y.Floor(),
+ +tr.bounds.Max.X.Ceil(),
+ -tr.bounds.Max.Y.Ceil(),
+ ))
+ return tr, nil
+}
+
+func (tr *textRenderer) addRune(r rune) bool {
+ dr, mask, maskp, advance, ok := tr.face.Glyph(
+ fixed.P(0, 0) /* subpixel destination location */, r)
+ if !ok {
+ return false
+ }
+
+ for i := 0; i < len(tr.buf.Pix); i++ {
+ tr.buf.Pix[i] = 0
+ }
+
+ // Copying, since there are absolutely no guarantees.
+ draw.Draw(tr.buf, dr, mask, maskp, draw.Src)
+
+ _ = render.AddGlyphs(tr.X, tr.gsid, 1, []uint32{uint32(r)},
+ []render.Glyphinfo{{
+ Width: uint16(tr.buf.Rect.Size().X),
+ Height: uint16(tr.buf.Rect.Size().Y),
+ X: int16(-tr.bounds.Min.X.Floor()),
+ Y: int16(+tr.bounds.Max.Y.Ceil()),
+ XOff: int16(advance.Ceil()),
+ YOff: int16(0),
+ }}, []byte(tr.buf.Pix))
+ return true
+}
+
+func (tr *textRenderer) render(src, dst render.Picture,
+ srcX, srcY, destX, destY int16, text string) xgbCookie {
+ // XXX: You're really supposed to handle tabs differently from this.
+ text = strings.Replace(text, "\t", " ", -1)
+
+ for _, r := range text {
+ if !tr.loaded[r] {
+ tr.addRune(r)
+ tr.loaded[r] = true
+ }
+ }
+
+ return compositeString(tr.X, render.PictOpOver, src, dst,
+ 0 /* TODO: mask Pictureformat? */, tr.gsid,
+ srcX, srcY, destX, destY, text)
+}
+
+const (
+ ksEscape = 0xff1b
+ ksUp = 0xff52
+ ksDown = 0xff54
+ ksPageUp = 0xff55
+ ksPageDown = 0xff56
+ ksModeSwitch = 0xff7e
+)
+
+type keyMapper struct {
+ X *xgb.Conn
+ setup *xproto.SetupInfo
+ mapping *xproto.GetKeyboardMappingReply
+
+ modeSwitchMask uint16
+}
+
+func newKeyMapper(X *xgb.Conn) (*keyMapper, error) {
+ m := &keyMapper{X: X, setup: xproto.Setup(X)}
+ if err := m.update(); err != nil {
+ return nil, err
+ }
+ return m, nil
+}
+
+func (km *keyMapper) update() error {
+ var err error
+ km.mapping, err = xproto.GetKeyboardMapping(km.X, km.setup.MinKeycode,
+ byte(km.setup.MaxKeycode-km.setup.MinKeycode+1)).Reply()
+ if err != nil {
+ return err
+ }
+
+ km.modeSwitchMask = 0
+
+ // The order is "Shift, Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5."
+ mm, err := xproto.GetModifierMapping(km.X).Reply()
+ if err != nil {
+ return err
+ }
+
+ perMod := int(mm.KeycodesPerModifier)
+ perKc := int(km.mapping.KeysymsPerKeycode)
+ for mod := 0; mod < 8; mod++ {
+ for _, kc := range mm.Keycodes[mod*perMod : (mod+1)*perMod] {
+ if kc == 0 {
+ continue
+ }
+
+ k := int(kc - km.setup.MinKeycode)
+ for _, ks := range km.mapping.Keysyms[k*perKc : (k+1)*perKc] {
+ if ks == ksModeSwitch {
+ km.modeSwitchMask |= 1 << uint(mod)
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func (km *keyMapper) decode(e xproto.KeyPressEvent) (result xproto.Keysym) {
+ step := int(km.mapping.KeysymsPerKeycode)
+ from := int(e.Detail-km.setup.MinKeycode) * step
+ ks := km.mapping.Keysyms[from : from+step]
+
+ // Strip trailing NoSymbol entries.
+ for len(ks) > 0 && ks[len(ks)-1] == 0 {
+ ks = ks[:len(ks)-1]
+ }
+
+ // Expand back to at least 4.
+ switch {
+ case len(ks) == 1:
+ ks = append(ks, 0, ks[0], 0)
+ case len(ks) == 2:
+ ks = append(ks, ks[0], ks[1])
+ case len(ks) == 3:
+ ks = append(ks, 0)
+ }
+
+ // Other silly expansion rules, only applied to basic ASCII since we
+ // don't have translation tables to Unicode here for brevity.
+ if ks[1] == 0 {
+ ks[1] = ks[0]
+ if ks[0] >= 'A' && ks[0] <= 'Z' ||
+ ks[0] >= 'a' && ks[0] <= 'z' {
+ ks[0] = ks[0] | 32
+ ks[1] = ks[0] &^ 32
+ }
+ }
+
+ if ks[3] == 0 {
+ ks[3] = ks[2]
+ if ks[2] >= 'A' && ks[2] <= 'Z' ||
+ ks[2] >= 'a' && ks[2] <= 'z' {
+ ks[2] = ks[2] | 32
+ ks[3] = ks[2] &^ 32
+ }
+ }
+
+ offset := 0
+ if e.State&km.modeSwitchMask != 0 {
+ offset += 2
+ }
+
+ shift := e.State&xproto.ModMaskShift != 0
+ lock := e.State&xproto.ModMaskLock != 0
+ switch {
+ case !shift && !lock:
+ result = ks[offset+0]
+ case !shift && lock:
+ if ks[offset+0] >= 'a' && ks[offset+0] <= 'z' {
+ result = ks[offset+1]
+ } else {
+ result = ks[offset+0]
+ }
+ case shift && lock:
+ if ks[offset+1] >= 'a' && ks[offset+1] <= 'z' {
+ result = ks[offset+1] &^ 32
+ } else {
+ result = ks[offset+1]
+ }
+ case shift:
+ result = ks[offset+1]
+ }
+ return
+}
+
+func main() {
+ if len(os.Args) < 2 {
+ log.Fatalln("no filename given")
+ }
+
+ text, err := ioutil.ReadFile(os.Args[1])
+ if err != nil {
+ log.Fatalln(err)
+ }
+ lines := strings.Split(string(text), "\n")
+
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+ if err := render.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ visual, depth := screen.RootVisual, screen.RootDepth
+ // TODO: We should check that we find it, though we don't /need/ alpha here,
+ // it's just a minor improvement--affects the backpixel value.
+ for _, i := range screen.AllowedDepths {
+ // TODO: Could/should check other parameters.
+ for _, v := range i.Visuals {
+ if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor {
+ visual, depth = v.VisualId, i.Depth
+ break
+ }
+ }
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask|
+ xproto.CwColormap, []uint32{0xf0f0f0f0, 0,
+ xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
+ /* KeymapNotify */ xproto.EventMaskKeymapState |
+ xproto.EventMaskExposure | xproto.EventMaskButtonPress,
+ uint32(mid)})
+
+ title := []byte("Viewer")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Similar to XRenderFindVisualFormat.
+ // The DefaultScreen is almost certain to be zero.
+ var pformat render.Pictformat
+ for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
+ // This check seems to be slightly extraneous.
+ if pd.Depth != depth {
+ continue
+ }
+ for _, pv := range pd.Visuals {
+ if pv.Visual == visual {
+ pformat = pv.Format
+ }
+ }
+ }
+
+ pid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
+
+ blackid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ _ = render.CreateSolidFill(X, blackid, render.Color{Alpha: 0xffff})
+
+ tr, err := newTextRenderer(X, goregular.TTF, &truetype.Options{
+ Size: 10,
+ DPI: float64(screen.WidthInPixels) /
+ float64(screen.WidthInMillimeters) * 25.4,
+ Hinting: font.HintingFull,
+ })
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ scroll := 0 // index of the top line
+
+ var w, h uint16
+ redraw := func() {
+ y, ascent, step := 5, tr.bounds.Max.Y.Ceil(),
+ tr.bounds.Max.Y.Ceil()-tr.bounds.Min.Y.Floor()
+ for _, line := range lines[scroll:] {
+ if uint16(y) >= h {
+ break
+ }
+ _ = tr.render(blackid, pid, 0, 0, 5, int16(y+ascent), line)
+ y += step
+ }
+
+ vis := float64(h-10) / float64(step)
+ if vis < float64(len(lines)) {
+ length := float64(step) * (vis + 1) * vis / float64(len(lines))
+ start := float64(step) * float64(scroll) * vis / float64(len(lines))
+
+ _ = render.FillRectangles(X, render.PictOpSrc, pid,
+ render.Color{Alpha: 0xffff}, []xproto.Rectangle{{
+ X: int16(w - 15), Y: int16(start),
+ Width: 15, Height: uint16(length + 10)}})
+ }
+ }
+
+ km, err := newKeyMapper(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ConfigureNotifyEvent:
+ w, h = e.Width, e.Height
+
+ case xproto.MappingNotifyEvent:
+ _ = km.update()
+
+ case xproto.KeyPressEvent:
+ _ = xproto.ClearArea(X, true /* ExposeEvent */, wid, 0, 0, w, h)
+
+ const pageJump = 40
+ switch km.decode(e) {
+ case ksEscape:
+ return
+ case ksUp:
+ if scroll >= 1 {
+ scroll--
+ }
+ case ksDown:
+ if scroll+1 < len(lines) {
+ scroll++
+ }
+ case ksPageUp:
+ if scroll >= pageJump {
+ scroll -= pageJump
+ }
+ case ksPageDown:
+ if scroll+pageJump < len(lines) {
+ scroll += pageJump
+ }
+ }
+
+ case xproto.ButtonPressEvent:
+ _ = xproto.ClearArea(X, true /* ExposeEvent */, wid, 0, 0, w, h)
+
+ switch e.Detail {
+ case xproto.ButtonIndex4:
+ if scroll > 0 {
+ scroll--
+ }
+ case xproto.ButtonIndex5:
+ if scroll+1 < len(lines) {
+ scroll++
+ }
+ }
+
+ case xproto.ExposeEvent:
+ // FIXME: The window's context haven't necessarily been destroyed.
+ if e.Count == 0 {
+ redraw()
+ }
+ }
+ }
+}
diff --git a/prototypes/xgb-window.go b/prototypes/xgb-window.go
new file mode 100644
index 0000000..3944fa4
--- /dev/null
+++ b/prototypes/xgb-window.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/xproto"
+ "log"
+ "math"
+ "math/rand"
+)
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ var visual xproto.Visualid
+ var depth byte
+ for _, i := range screen.AllowedDepths {
+ if i.Depth == 32 {
+ // TODO: Could/should check other parameters.
+ for _, v := range i.Visuals {
+ if v.Class == xproto.VisualClassTrueColor {
+ visual = v.VisualId
+ depth = i.Depth
+ break
+ }
+ }
+ }
+ }
+ if visual == 0 {
+ log.Fatalln("cannot find an RGBA TrueColor visual")
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBorderPixel|xproto.CwColormap,
+ []uint32{0, uint32(mid)})
+
+ // This could be included in CreateWindow parameters.
+ _ = xproto.ChangeWindowAttributes(X, wid,
+ xproto.CwBackPixel|xproto.CwEventMask, []uint32{0x80808080,
+ xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
+ xproto.EventMaskExposure})
+
+ title := "Gradient"
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), []byte(title))
+
+ _ = xproto.MapWindow(X, wid)
+
+ cid, err := xproto.NewGcontextId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateGC(X, cid, xproto.Drawable(wid),
+ xproto.GcGraphicsExposures, []uint32{0})
+
+ blend := func(a, b uint32, ratio, gamma float64) uint32 {
+ iratio := 1 - ratio
+
+ fa := math.Pow(float64(a)/255, gamma)
+ fb := math.Pow(float64(b)/255, gamma)
+
+ return uint32(math.Pow(ratio*fa+iratio*fb, 1/gamma)*255) & 0xff
+ }
+
+ // TODO: We could show some text just like we intend to with xgb-xrender.go.
+
+ var w, h uint16
+ var start, end uint32 = 0xabcdef, 0x32ab54
+ gradient := func() {
+ ra, ga, ba := (start>>16)&0xff, (start>>8)&0xff, start&0xff
+ rb, gb, bb := (end>>16)&0xff, (end>>8)&0xff, end&0xff
+
+ var low, high uint16 = 50, h - 50
+ if high > h {
+ return
+ }
+
+ for y := low; y < high; y++ {
+ ratio := float64(y-low) / (float64(high) - float64(low))
+
+ rR := blend(ra, rb, ratio, 2.2)
+ gG := blend(ga, gb, ratio, 2.2)
+ bB := blend(ba, bb, ratio, 2.2)
+
+ _ = xproto.ChangeGC(X, cid, xproto.GcForeground,
+ []uint32{0xff000000 | rR<<16 | gG<<8 | bB})
+ _ = xproto.PolyLine(X, xproto.CoordModeOrigin, xproto.Drawable(wid),
+ cid, []xproto.Point{
+ {X: 50, Y: int16(y)},
+ {X: int16(w / 2), Y: int16(y)},
+ })
+
+ rR = blend(ra, rb, ratio, 1)
+ gG = blend(ga, gb, ratio, 1)
+ bB = blend(ba, bb, ratio, 1)
+
+ _ = xproto.ChangeGC(X, cid, xproto.GcForeground,
+ []uint32{0xff000000 | rR<<16 | gG<<8 | bB})
+ _ = xproto.PolyLine(X, xproto.CoordModeOrigin, xproto.Drawable(wid),
+ cid, []xproto.Point{
+ {X: int16(w / 2), Y: int16(y)},
+ {X: int16(w - 50), Y: int16(y)},
+ })
+ }
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ConfigureNotifyEvent:
+ w, h = e.Width, e.Height
+
+ case xproto.KeyPressEvent:
+ start = rand.Uint32() & 0xffffff
+ end = rand.Uint32() & 0xffffff
+ gradient()
+
+ case xproto.ExposeEvent:
+ gradient()
+ }
+ }
+
+}
diff --git a/prototypes/xgb-xrender.go b/prototypes/xgb-xrender.go
new file mode 100644
index 0000000..ad768b7
--- /dev/null
+++ b/prototypes/xgb-xrender.go
@@ -0,0 +1,397 @@
+package main
+
+// We could also easily use x/image/font/basicfont to load some glyphs into X,
+// relying on the fact that it is a vertical array of A8 masks. Though it only
+// supports ASCII and has but one size. Best just make a custom BDF loader,
+// those fonts have larger coverages and we would be in control. Though again,
+// they don't seem to be capable of antialiasing.
+
+import (
+ "fmt"
+ "github.com/BurntSushi/xgb"
+ "github.com/BurntSushi/xgb/render"
+ "github.com/BurntSushi/xgb/xproto"
+ // golang.org/x/image/font/opentype cannot render yet but the API is
+ // more or less the same.
+ "github.com/golang/freetype"
+ "github.com/golang/freetype/truetype"
+ "golang.org/x/image/font"
+ "golang.org/x/image/font/gofont/goregular"
+ "golang.org/x/image/math/fixed"
+ "image"
+ "image/draw"
+ "log"
+ "math/rand"
+)
+
+func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
+func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
+
+func glyphListBytes(buf []byte, runes []rune, size int) int {
+ b := 0
+ for _, r := range runes {
+ switch size {
+ default:
+ buf[b] = byte(r)
+ b += 1
+ case 2:
+ xgb.Put16(buf[b:], uint16(r))
+ b += 2
+ case 4:
+ xgb.Put32(buf[b:], uint32(r))
+ b += 4
+ }
+ }
+ return xgb.Pad(b)
+}
+
+// When the len is 255, a GLYPHABLE follows, otherwise a list of CARD8/16/32.
+func glyphEltHeaderBytes(buf []byte, len byte, deltaX, deltaY int16) int {
+ b := 0
+ buf[b] = len
+ b += 4
+ xgb.Put16(buf[b:], uint16(deltaX))
+ b += 2
+ xgb.Put16(buf[b:], uint16(deltaY))
+ b += 2
+ return xgb.Pad(b)
+}
+
+type xgbCookie interface{ Check() error }
+
+// TODO: We actually need a higher-level function that also keeps track of
+// and loads glyphs into the X server.
+// TODO: We also need a way to use kerning tables with this, inserting/removing
+// advance pixels between neighboring characters.
+
+// compositeString makes an appropriate render.CompositeGlyphs request,
+// assuming that glyphs equal Unicode codepoints.
+func compositeString(c *xgb.Conn, op byte, src, dst render.Picture,
+ maskFormat render.Pictformat, glyphset render.Glyphset, srcX, srcY int16,
+ destX, destY int16, text string) xgbCookie {
+ runes := []rune(text)
+
+ var highest rune
+ for _, r := range runes {
+ if r > highest {
+ highest = r
+ }
+ }
+
+ size := 1
+ switch {
+ case highest > 1<<16:
+ size = 4
+ case highest > 1<<8:
+ size = 2
+ }
+
+ // They gave up on the XCB protocol API and we need to serialize explicitly.
+
+ // To spare us from caring about the padding, use the largest number lesser
+ // than 255 that is divisible by 4 (for size 2 and 4 the requirements are
+ // less strict but this works in the general case).
+ const maxPerChunk = 252
+
+ buf := make([]byte, (len(runes)+maxPerChunk-1)/maxPerChunk*8+len(runes)*size)
+ b := 0
+
+ for len(runes) > maxPerChunk {
+ b += glyphEltHeaderBytes(buf[b:], maxPerChunk, 0, 0)
+ b += glyphListBytes(buf[b:], runes[:maxPerChunk], size)
+ runes = runes[maxPerChunk:]
+ }
+ if len(runes) > 0 {
+ b += glyphEltHeaderBytes(buf[b:], byte(len(runes)), destX, destY)
+ b += glyphListBytes(buf[b:], runes, size)
+ }
+
+ switch size {
+ default:
+ return render.CompositeGlyphs8(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ case 2:
+ return render.CompositeGlyphs16(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ case 4:
+ return render.CompositeGlyphs32(c, op, src, dst, maskFormat, glyphset,
+ srcX, srcY, buf)
+ }
+}
+
+func main() {
+ X, err := xgb.NewConn()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := render.Init(X); err != nil {
+ log.Fatalln(err)
+ }
+
+ setup := xproto.Setup(X)
+ screen := setup.DefaultScreen(X)
+
+ var visual xproto.Visualid
+ var depth byte
+ for _, i := range screen.AllowedDepths {
+ if i.Depth == 32 {
+ // TODO: Could/should check other parameters.
+ for _, v := range i.Visuals {
+ if v.Class == xproto.VisualClassTrueColor {
+ visual = v.VisualId
+ depth = i.Depth
+ break
+ }
+ }
+ }
+ }
+ if visual == 0 {
+ log.Fatalln("cannot find an RGBA TrueColor visual")
+ }
+
+ mid, err := xproto.NewColormapId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = xproto.CreateColormap(
+ X, xproto.ColormapAllocNone, mid, screen.Root, visual)
+
+ wid, err := xproto.NewWindowId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Border pixel and colormap are required when depth differs from parent.
+ _ = xproto.CreateWindow(X, depth, wid, screen.Root,
+ 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
+ visual, xproto.CwBorderPixel|xproto.CwColormap,
+ []uint32{0, uint32(mid)})
+
+ // This could be included in CreateWindow parameters.
+ _ = xproto.ChangeWindowAttributes(X, wid,
+ xproto.CwBackPixel|xproto.CwEventMask, []uint32{0x80808080,
+ xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
+ xproto.EventMaskExposure})
+
+ title := []byte("Gradient")
+ _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
+ xproto.AtomString, 8, uint32(len(title)), title)
+
+ _ = xproto.MapWindow(X, wid)
+
+ /*
+ rfilters, err := render.QueryFilters(X, xproto.Drawable(wid)).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ filters := []string{}
+ for _, f := range rfilters.Filters {
+ filters = append(filters, f.Name)
+ }
+
+ log.Printf("filters: %v\n", filters)
+ */
+
+ pformats, err := render.QueryPictFormats(X).Reply()
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ /*
+ for _, pf := range pformats.Formats {
+ log.Printf("format %2d: depth %2d, RGBA %3x %3x %3x %3x\n",
+ pf.Id, pf.Depth,
+ pf.Direct.RedMask, pf.Direct.GreenMask, pf.Direct.BlueMask,
+ pf.Direct.AlphaMask)
+ }
+ */
+
+ // Similar to XRenderFindVisualFormat.
+ // The DefaultScreen is almost certain to be zero.
+ var pformat render.Pictformat
+ for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
+ // This check seems to be slightly extraneous.
+ if pd.Depth != depth {
+ continue
+ }
+ for _, pv := range pd.Visuals {
+ if pv.Visual == visual {
+ pformat = pv.Format
+ }
+ }
+ }
+
+ // ...or just scan through pformats.Formats and look for matches, which is
+ // what XRenderFindStandardFormat in Xlib does as well as exp/shiny.
+
+ f, err := freetype.ParseFont(goregular.TTF)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // LCD subpixel rendering isn't supported. :(
+ opts := &truetype.Options{
+ Size: 10,
+ DPI: 96, // TODO: Take this from the screen or monitor.
+ Hinting: font.HintingFull,
+ }
+ face := truetype.NewFace(f, opts)
+ bounds := f.Bounds(fixed.Int26_6(opts.Size * float64(opts.DPI) *
+ (64.0 / 72.0)))
+
+ var rgbFormat render.Pictformat
+ for _, pf := range pformats.Formats {
+ // Hopefully. Might want to check byte order.
+ if pf.Depth == 32 && pf.Direct.AlphaMask != 0 {
+ rgbFormat = pf.Id
+ break
+ }
+ }
+
+ gsid, err := render.NewGlyphsetId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // NOTE: A depth of 24 will not work, the server always rejects it.
+ // Composite alpha doesn't make sense since golang/freetype can't use it.
+ // We use RGBA here just so that lines are padded to 32 bits.
+ _ = render.CreateGlyphSet(X, gsid, rgbFormat)
+
+ // NOTE: We could do gamma post-correction in higher precision if we
+ // implemented our own clone of the image.Image implementation.
+ nrgb := image.NewRGBA(image.Rect(
+ +bounds.Min.X.Floor(),
+ -bounds.Min.Y.Floor(),
+ +bounds.Max.X.Ceil(),
+ -bounds.Max.Y.Ceil(),
+ ))
+
+ for r := rune(32); r < 128; r++ {
+ dr, mask, maskp, advance, ok := face.Glyph(
+ fixed.P(0, 0) /* subpixel destination location */, r)
+ if !ok {
+ log.Println("skip")
+ continue
+ }
+
+ for i := 0; i < len(nrgb.Pix); i++ {
+ nrgb.Pix[i] = 0
+ }
+
+ draw.Draw(nrgb, dr, mask, maskp, draw.Src)
+
+ _ = render.AddGlyphs(X, gsid, 1, []uint32{uint32(r)},
+ []render.Glyphinfo{{
+ Width: uint16(nrgb.Rect.Size().X),
+ Height: uint16(nrgb.Rect.Size().Y),
+ X: int16(-bounds.Min.X.Floor()),
+ Y: int16(+bounds.Max.Y.Ceil()),
+ XOff: int16(advance.Ceil()),
+ YOff: int16(0),
+ }}, []byte(nrgb.Pix))
+ }
+
+ pid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ // Dithering is not supported. :(
+ render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
+
+ // Reserve an ID for the gradient.
+ gid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ whiteid, err := render.NewPictureId(X)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ _ = render.CreateSolidFill(X, whiteid, render.Color{
+ Red: 0xffff,
+ Green: 0xffff,
+ Blue: 0xffff,
+ Alpha: 0xffff,
+ })
+
+ var from, to render.Color
+ var start, end uint32
+ recolor := func() {
+ start = rand.Uint32() & 0xffffff
+ from = render.Color{
+ Red: 0x101 * uint16((start>>16)&0xff),
+ Green: 0x101 * uint16((start>>8)&0xff),
+ Blue: 0x101 * uint16(start&0xff),
+ Alpha: 0xffff,
+ }
+
+ end = rand.Uint32() & 0xffffff
+ to = render.Color{
+ Red: 0x101 * uint16((end>>16)&0xff),
+ Green: 0x101 * uint16((end>>8)&0xff),
+ Blue: 0x101 * uint16(end&0xff),
+ Alpha: 0xffff,
+ }
+ }
+
+ var w, h uint16
+ gradient := func() {
+ if w < 100 || h < 100 {
+ return
+ }
+
+ // We could also use a transformation matrix for changes in size.
+ _ = render.CreateLinearGradient(X, gid,
+ render.Pointfix{F64ToFixed(0), F64ToFixed(0)},
+ render.Pointfix{F64ToFixed(0), F64ToFixed(float64(h) - 100)},
+ 2, []render.Fixed{F64ToFixed(0), F64ToFixed(1)},
+ []render.Color{from, to})
+
+ _ = render.Composite(X, render.PictOpSrc, gid, render.PictureNone, pid,
+ 0, 0, 0, 0, 50, 50, w-100, h-100)
+
+ _ = render.FreePicture(X, gid)
+
+ _ = compositeString(X, render.PictOpOver, whiteid, pid,
+ 0 /* TODO: mask Pictureformat? */, gsid, 0, 0, 100, 100,
+ fmt.Sprintf("%#06x - %#06x", start, end))
+ _ = compositeString(X, render.PictOpOver, whiteid, pid,
+ 0 /* TODO: mask Pictureformat? */, gsid, 0, 0, 100, 150,
+ "The quick brown fox jumps over the lazy dog.")
+ }
+
+ for {
+ ev, xerr := X.WaitForEvent()
+ if xerr != nil {
+ log.Printf("Error: %s\n", xerr)
+ return
+ }
+ if ev == nil {
+ return
+ }
+
+ log.Printf("Event: %s\n", ev)
+ switch e := ev.(type) {
+ case xproto.UnmapNotifyEvent:
+ return
+
+ case xproto.ConfigureNotifyEvent:
+ w, h = e.Width, e.Height
+ recolor()
+
+ case xproto.KeyPressEvent:
+ recolor()
+ gradient()
+
+ case xproto.ExposeEvent:
+ gradient()
+ }
+ }
+}