diff options
Diffstat (limited to 'prototypes')
| -rw-r--r-- | prototypes/tls-autodetect.go | 451 | ||||
| -rw-r--r-- | prototypes/xgb-draw.go | 329 | ||||
| -rw-r--r-- | prototypes/xgb-image.go | 313 | ||||
| -rw-r--r-- | prototypes/xgb-keys.go | 213 | ||||
| -rw-r--r-- | prototypes/xgb-monitors.go | 40 | ||||
| -rw-r--r-- | prototypes/xgb-text-viewer.go | 538 | ||||
| -rw-r--r-- | prototypes/xgb-window.go | 156 | ||||
| -rw-r--r-- | prototypes/xgb-xrender.go | 397 | 
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() +		} +	} +} | 
