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