From 3e42402e2bdeb665b555919cf57ad3258ef103e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sun, 26 Aug 2018 03:44:23 +0200 Subject: [PATCH] xgb-text-viewer: add a demo text viewer More of a real application and just needs pictures in order to bring the parts I have so far all together. --- prototypes/xgb-text-viewer.go | 538 ++++++++++++++++++++++++++++++++++ 1 file changed, 538 insertions(+) create mode 100644 prototypes/xgb-text-viewer.go 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() + } + } + } +}