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.
This commit is contained in:
parent
c8fd1068d1
commit
3e42402e2b
538
prototypes/xgb-text-viewer.go
Normal file
538
prototypes/xgb-text-viewer.go
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user