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:
		
							
								
								
									
										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()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user