You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
538 lines
13 KiB
538 lines
13 KiB
// 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() |
|
} |
|
} |
|
} |
|
}
|
|
|