diff options
Diffstat (limited to 'prototypes/xgb-xrender.go')
-rw-r--r-- | prototypes/xgb-xrender.go | 397 |
1 files changed, 397 insertions, 0 deletions
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() + } + } +} |