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() } } }