Compare commits

..

17 Commits

Author SHA1 Message Date
3e42402e2b
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.
2018-09-02 18:25:38 +02:00
c8fd1068d1
xgb-image: add support for the MIT-SHM extension 2018-09-02 18:25:37 +02:00
cea1792913
xgb-image: add a demo that shows a scaled picture 2018-09-02 18:25:37 +02:00
ff7518c74d
xgb-keys: minimal example of reading keys 2018-09-02 18:25:37 +02:00
9e070e9648
xgb-monitors: add an experimental dumper 2018-09-02 18:25:36 +02:00
0c2853a8ae
xgb-render: update comments 2018-09-02 18:25:36 +02:00
41e04fdc9f
xgb-render: go back to RGBA because of alignment
Size 9 just happened to have the buffer 16 bytes wide.
2018-09-02 18:25:36 +02:00
23586eae01
xgb-render: give up on composite alpha 2018-09-02 18:25:35 +02:00
9424579c75
xgb-render: cleanup, tolerable glyph placement 2018-09-02 18:25:35 +02:00
32beda3c90
xgb-render: slightly simplify 2018-09-02 18:25:35 +02:00
30f2366f9a
xgb-render: preliminary text rendering
I have finally got it working at all, now let's fix bounds etc.
2018-09-02 18:25:34 +02:00
215e3e8630
xgb-render: add some comments
Some containing code to list out potentially interesting information
from the X server.
2018-09-02 18:25:34 +02:00
44b01ccb17
xgb-window: add comparison with correct blending 2018-09-02 18:25:28 +02:00
0f7fcca7ce
xgb-xrender: add a basic demo for XRender
So far just a conversion of xgb-window.go.
2018-09-02 18:24:14 +02:00
1fdf14f351
xgb-window: add a basic xgb demo
Demonstrating RGBA visuals and direct pixel values.
2018-09-02 18:24:14 +02:00
0ef66c7282
Update README 2018-09-02 18:24:14 +02:00
68d7e34b03
hnc: cleanup 2018-08-06 21:58:32 +02:00
8 changed files with 1669 additions and 2 deletions

12
README
View File

@ -40,6 +40,7 @@ figured out it would make sense:
- hsm - system monitor - hsm - system monitor
- hss - spreadsheets - hss - spreadsheets
- htd - translation dictionary - htd - translation dictionary
- ht - terminal emulator
- htk - GUI toolkit library - htk - GUI toolkit library
See Projects for more information about the individual projects. See Projects for more information about the individual projects.
@ -50,7 +51,7 @@ Identity
-------- --------
The name merely hints at motivations and otherwise isn't of any practical The name merely hints at motivations and otherwise isn't of any practical
significance other than that we need an identifier. This time I resisted using significance other than that we need an identifier. This time I resisted using
something offensive. something offensive for personal amusement.
A logo covering the entire project is not needed and it seems hard to figure out A logo covering the entire project is not needed and it seems hard to figure out
anything meaninful anyway, though we might pick a specific font to use for the anything meaninful anyway, though we might pick a specific font to use for the
@ -184,6 +185,9 @@ only the per-buffer input line is going to be desynchronized.
https://godoc.org/github.com/gorilla/websocket https://godoc.org/github.com/gorilla/websocket
The higher-level client-server API could be made rather generic to allow for
smooth integration with non-IRC "backends" such as Slack or Mattermost.
htd -- translation dictionary htd -- translation dictionary
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This specific kind of application doesn't need a lot of user interface either, This specific kind of application doesn't need a lot of user interface either,
@ -221,6 +225,8 @@ In the second stage, support for the Language Server Protocol will be added so
that the project can be edited using its own tools. Some scripting, perhaps that the project can be edited using its own tools. Some scripting, perhaps
a tiny subset of VimL, might be desirable. Or other means of configuration. a tiny subset of VimL, might be desirable. Or other means of configuration.
Visual block mode or the color column may still be implemented.
The real model for the editor is Qt Creator with FakeVIM, though this is not to The real model for the editor is Qt Creator with FakeVIM, though this is not to
be a clone of it, e.g. the various "Output" lists could be just special buffers, be a clone of it, e.g. the various "Output" lists could be just special buffers,
which may be have names starting on "// ". which may be have names starting on "// ".
@ -249,6 +255,10 @@ Indexing and search may be based on a common database, no need to get all fancy:
http://rachbelaid.com/postgres-full-text-search-is-good-enough/ http://rachbelaid.com/postgres-full-text-search-is-good-enough/
https://www.sqlite.org/fts3.html#full_text_index_queries (FTS4 seems better) https://www.sqlite.org/fts3.html#full_text_index_queries (FTS4 seems better)
ht -- terminal emulator
~~~~~~~~~~~~~~~~~~~~~~~
Similar scope to st(1). Clever display of internal padding for better looks.
The rest The rest
~~~~~~~~ ~~~~~~~~
Currently there are no significant, specific plans about the other applications. Currently there are no significant, specific plans about the other applications.

View File

@ -129,7 +129,7 @@ func main() {
} }
} }
if result.err != nil { if result.err != nil {
log("%s: %s", "stdin", result.err) log("stdin: %s", result.err)
fromUser = nil fromUser = nil
if err := conn.CloseWrite(); err != nil { if err := conn.CloseWrite(); err != nil {
log("remote: %s", err) log("remote: %s", err)

313
prototypes/xgb-image.go Normal file
View File

@ -0,0 +1,313 @@
package main
import (
"encoding/binary"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/render"
"github.com/BurntSushi/xgb/shm"
"github.com/BurntSushi/xgb/xproto"
"log"
"os"
"reflect"
"time"
"unsafe"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)
// #include <sys/ipc.h>
// #include <sys/shm.h>
import "C"
func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
func main() {
/*
pf, err := os.Create("pprof.out")
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(pf)
defer pprof.StopCPUProfile()
*/
// Load a picture from the command line.
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatalln(err)
}
defer f.Close()
img, name, err := image.Decode(f)
if err != nil {
log.Fatalln(err)
}
log.Println("image type is", name)
// Miscellaneous X11 initialization.
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 {
for _, v := range i.Visuals {
// TODO: Could/should check other parameters.
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{0x80808080, 0,
xproto.EventMaskStructureNotify | xproto.EventMaskExposure,
uint32(mid)})
title := []byte("Image")
_ = 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
}
}
}
// Wrap the window's surface in a picture.
pid, err := render.NewPictureId(X)
if err != nil {
log.Fatalln(err)
}
render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
// setup.BitmapFormatScanline{Pad,Unit} and setup.BitmapFormatBitOrder
// don't interest us here since we're only using Z format pixmaps.
for _, pf := range setup.PixmapFormats {
if pf.Depth == 32 {
if pf.BitsPerPixel != 32 || pf.ScanlinePad != 32 {
log.Fatalln("unsuported X server")
}
}
}
pixid, err := xproto.NewPixmapId(X)
if err != nil {
log.Fatalln(err)
}
_ = xproto.CreatePixmap(X, 32, pixid, xproto.Drawable(screen.Root),
uint16(img.Bounds().Dx()), uint16(img.Bounds().Dy()))
var bgraFormat render.Pictformat
wanted := render.Directformat{
RedShift: 16,
RedMask: 0xff,
GreenShift: 8,
GreenMask: 0xff,
BlueShift: 0,
BlueMask: 0xff,
AlphaShift: 24,
AlphaMask: 0xff,
}
for _, pf := range pformats.Formats {
if pf.Depth == 32 && pf.Direct == wanted {
bgraFormat = pf.Id
break
}
}
if bgraFormat == 0 {
log.Fatalln("ARGB format not found")
}
// We could also look for the inverse pictformat.
var encoding binary.ByteOrder
if setup.ImageByteOrder == xproto.ImageOrderMSBFirst {
encoding = binary.BigEndian
} else {
encoding = binary.LittleEndian
}
pixpicid, err := render.NewPictureId(X)
if err != nil {
log.Fatalln(err)
}
render.CreatePicture(X, pixpicid, xproto.Drawable(pixid), bgraFormat,
0, []uint32{})
// Do we really need this? :/
cid, err := xproto.NewGcontextId(X)
if err != nil {
log.Fatalln(err)
}
_ = xproto.CreateGC(X, cid, xproto.Drawable(pixid),
xproto.GcGraphicsExposures, []uint32{0})
bounds := img.Bounds()
Lstart := time.Now()
if err := shm.Init(X); err != nil {
log.Println("MIT-SHM unavailable")
// We're being lazy and resolve the 1<<16 limit of requests by sending
// a row at a time. The encoding is also done inefficiently.
// Also see xgbutil/xgraphics/xsurface.go.
row := make([]byte, bounds.Dx()*4)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, a := img.At(x, y).RGBA()
encoding.PutUint32(row[x*4:],
(a>>8)<<24|(r>>8)<<16|(g>>8)<<8|(b>>8))
}
_ = xproto.PutImage(X, xproto.ImageFormatZPixmap,
xproto.Drawable(pixid), cid, uint16(bounds.Dx()), 1,
0, int16(y),
0, 32, row)
}
} else {
rep, err := shm.QueryVersion(X).Reply()
if err != nil {
log.Fatalln(err)
}
if rep.PixmapFormat != xproto.ImageFormatZPixmap ||
!rep.SharedPixmaps {
log.Fatalln("MIT-SHM configuration unfit")
}
shmSize := bounds.Dx() * bounds.Dy() * 4
// As a side note, to clean up unreferenced segments (orphans):
// ipcs -m | awk '$6 == "0" { print $2 }' | xargs ipcrm shm
shmID := int(C.shmget(C.IPC_PRIVATE,
C.size_t(shmSize), C.IPC_CREAT|0777))
if shmID == -1 {
// TODO: We should handle this case by falling back to PutImage,
// if only because the allocation may hit a system limit.
log.Fatalln("memory allocation failed")
}
dataRaw := C.shmat(C.int(shmID), nil, 0)
defer C.shmdt(dataRaw)
defer C.shmctl(C.int(shmID), C.IPC_RMID, nil)
data := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
Data: uintptr(dataRaw), Len: shmSize, Cap: shmSize}))
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
row := data[y*bounds.Dx()*4:]
for x := bounds.Min.X; x < bounds.Max.X; x++ {
r, g, b, a := img.At(x, y).RGBA()
encoding.PutUint32(row[x*4:],
(a>>8)<<24|(r>>8)<<16|(g>>8)<<8|(b>>8))
}
}
segid, err := shm.NewSegId(X)
if err != nil {
log.Fatalln(err)
}
// Need to have it attached on the server before we unload the segment.
c := shm.AttachChecked(X, segid, uint32(shmID), true /* RO */)
if err := c.Check(); err != nil {
log.Fatalln(err)
}
_ = shm.PutImage(X, xproto.Drawable(pixid), cid,
uint16(bounds.Dx()), uint16(bounds.Dy()), 0, 0,
uint16(bounds.Dx()), uint16(bounds.Dy()), 0, 0,
32, xproto.ImageFormatZPixmap,
0 /* SendEvent */, segid, 0 /* Offset */)
}
log.Println("uploading took", time.Now().Sub(Lstart))
var scale float64 = 1
for {
ev, xerr := X.WaitForEvent()
if xerr != nil {
log.Printf("Error: %s\n", xerr)
return
}
if ev == nil {
return
}
log.Printf("Event: %s\n", ev)
switch e := ev.(type) {
case xproto.UnmapNotifyEvent:
return
case xproto.ConfigureNotifyEvent:
w, h := e.Width, e.Height
scaleX := float64(bounds.Dx()) / float64(w)
scaleY := float64(bounds.Dy()) / float64(h)
if scaleX < scaleY {
scale = scaleY
} else {
scale = scaleX
}
_ = render.SetPictureTransform(X, pixpicid, render.Transform{
F64ToFixed(scale), F64ToFixed(0), F64ToFixed(0),
F64ToFixed(0), F64ToFixed(scale), F64ToFixed(0),
F64ToFixed(0), F64ToFixed(0), F64ToFixed(1),
})
_ = render.SetPictureFilter(X, pixpicid, 8, "bilinear", nil)
case xproto.ExposeEvent:
_ = render.Composite(X, render.PictOpSrc,
pixpicid, render.PictureNone, pid,
0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
uint16(float64(img.Bounds().Dx())/scale),
uint16(float64(img.Bounds().Dy())/scale))
}
}
}

213
prototypes/xgb-keys.go Normal file
View File

@ -0,0 +1,213 @@
package main
import (
"github.com/BurntSushi/xgb"
//"github.com/BurntSushi/xgb/xkb"
"github.com/BurntSushi/xgb/xproto"
"log"
)
func main() {
X, err := xgb.NewConn()
if err != nil {
log.Fatalln(err)
}
/*
// Use the extension if available, makes better use of state bits.
if err := xkb.Init(X); err == nil {
if _, err := xkb.UseExtension(X, 1, 0).Reply(); 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 {
for _, v := range i.Visuals {
// TODO: Could/should check other parameters.
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{0x80808080, 0,
xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
/* KeymapNotify */ xproto.EventMaskKeymapState, uint32(mid)})
title := []byte("Keys")
_ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
xproto.AtomString, 8, uint32(len(title)), title)
_ = xproto.MapWindow(X, wid)
mapping, err := xproto.GetKeyboardMapping(X, setup.MinKeycode,
byte(setup.MaxKeycode-setup.MinKeycode+1)).Reply()
if err != nil {
log.Fatalln(err)
}
// The order is "Shift, Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5."
mm, err := xproto.GetModifierMapping(X).Reply()
if err != nil {
log.Fatalln(err)
}
// XXX: This seems pointless, the key will just end up switching groups
// instead of levels without full XKB handling. Though perhaps it might
// at least work as intended when there's only one XKB group.
const MODE_SWITCH = 0xff7e
var modeSwitchMask uint16
for mod := 0; mod < 8; mod++ {
perMod := int(mm.KeycodesPerModifier)
for _, kc := range mm.Keycodes[mod*perMod : (mod+1)*perMod] {
if kc == 0 {
continue
}
perKc := int(mapping.KeysymsPerKeycode)
k := int(kc - setup.MinKeycode)
for _, ks := range mapping.Keysyms[k*perKc : (k+1)*perKc] {
if ks == MODE_SWITCH {
modeSwitchMask |= 1 << uint(mod)
}
}
}
}
for {
ev, xerr := X.WaitForEvent()
if xerr != nil {
log.Printf("Error: %s\n", xerr)
return
}
if ev == nil {
return
}
log.Printf("Event: %s\n", ev)
switch e := ev.(type) {
case xproto.UnmapNotifyEvent:
return
case xproto.KeymapNotifyEvent:
// e.Keys is a 32 * 8 large bitmap indicating which keys are
// currently pressed down. This is sent "after every EnterNotify
// and FocusIn" but it also seems to fire when the keyboard layout
// changes. aixterm manual even speaks of that explicitly.
//
// But since changing the effective group involves no changes to
// the compatibility mapping, there's nothing for us to do.
case xproto.MappingNotifyEvent:
// e.FirstKeyCode .. e.Count changes have happened but rereading
// everything is the simpler thing to do.
mapping, err = xproto.GetKeyboardMapping(X, setup.MinKeycode,
byte(setup.MaxKeycode-setup.MinKeycode+1)).Reply()
if err != nil {
log.Fatalln(err)
}
// TODO: We should also repeat the search for MODE SWITCH.
case xproto.KeyPressEvent:
step := int(mapping.KeysymsPerKeycode)
from := int(e.Detail-setup.MinKeycode) * step
ks := 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.
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
}
}
// We only have enough information to switch between two groups.
offset := 0
if e.State&modeSwitchMask != 0 {
offset += 2
}
var result xproto.Keysym
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]
}
if result <= 0xff {
log.Printf("%c (Latin-1)\n", rune(result))
} else {
log.Println(result)
}
}
}
}

View File

@ -0,0 +1,40 @@
package main
import (
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
"log"
// Needs a patched local version with xcb-proto 1.12 and this fix:
// -size := xgb.Pad((8 + (24 + xgb.Pad((int(NOutput) * 4)))))
// +size := xgb.Pad((8 + (24 + xgb.Pad((int(Monitorinfo.NOutput) * 4)))))
"github.com/BurntSushi/xgb/randr"
)
func main() {
X, err := xgb.NewConn()
if err != nil {
log.Fatalln(err)
}
if err := randr.Init(X); err != nil {
log.Fatalln(err)
}
setup := xproto.Setup(X)
screen := setup.DefaultScreen(X)
ms, err := randr.GetMonitors(X, screen.Root, true /* GetActive */).Reply()
if err != nil {
log.Fatalln(err)
}
for _, m := range ms.Monitors {
reply, err := xproto.GetAtomName(X, m.Name).Reply()
if err != nil {
log.Fatalln(err)
}
log.Printf("Monitor %s %+v\n", reply.Name, m)
}
}

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

156
prototypes/xgb-window.go Normal file
View File

@ -0,0 +1,156 @@
package main
import (
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/xproto"
"log"
"math"
"math/rand"
)
func main() {
X, err := xgb.NewConn()
if err != nil {
log.Fatalln(err)
}
setup := xproto.Setup(X)
screen := setup.DefaultScreen(X)
var visual xproto.Visualid
var depth byte
for _, i := range screen.AllowedDepths {
if i.Depth == 32 {
// TODO: Could/should check other parameters.
for _, v := range i.Visuals {
if v.Class == xproto.VisualClassTrueColor {
visual = v.VisualId
depth = i.Depth
break
}
}
}
}
if visual == 0 {
log.Fatalln("cannot find an RGBA TrueColor visual")
}
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.CwBorderPixel|xproto.CwColormap,
[]uint32{0, uint32(mid)})
// This could be included in CreateWindow parameters.
_ = xproto.ChangeWindowAttributes(X, wid,
xproto.CwBackPixel|xproto.CwEventMask, []uint32{0x80808080,
xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
xproto.EventMaskExposure})
title := "Gradient"
_ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
xproto.AtomString, 8, uint32(len(title)), []byte(title))
_ = xproto.MapWindow(X, wid)
cid, err := xproto.NewGcontextId(X)
if err != nil {
log.Fatalln(err)
}
_ = xproto.CreateGC(X, cid, xproto.Drawable(wid),
xproto.GcGraphicsExposures, []uint32{0})
blend := func(a, b uint32, ratio, gamma float64) uint32 {
iratio := 1 - ratio
fa := math.Pow(float64(a)/255, gamma)
fb := math.Pow(float64(b)/255, gamma)
return uint32(math.Pow(ratio*fa+iratio*fb, 1/gamma)*255) & 0xff
}
// TODO: We could show some text just like we intend to with xgb-xrender.go.
var w, h uint16
var start, end uint32 = 0xabcdef, 0x32ab54
gradient := func() {
ra, ga, ba := (start>>16)&0xff, (start>>8)&0xff, start&0xff
rb, gb, bb := (end>>16)&0xff, (end>>8)&0xff, end&0xff
var low, high uint16 = 50, h - 50
if high > h {
return
}
for y := low; y < high; y++ {
ratio := float64(y-low) / (float64(high) - float64(low))
rR := blend(ra, rb, ratio, 2.2)
gG := blend(ga, gb, ratio, 2.2)
bB := blend(ba, bb, ratio, 2.2)
_ = xproto.ChangeGC(X, cid, xproto.GcForeground,
[]uint32{0xff000000 | rR<<16 | gG<<8 | bB})
_ = xproto.PolyLine(X, xproto.CoordModeOrigin, xproto.Drawable(wid),
cid, []xproto.Point{
{X: 50, Y: int16(y)},
{X: int16(w / 2), Y: int16(y)},
})
rR = blend(ra, rb, ratio, 1)
gG = blend(ga, gb, ratio, 1)
bB = blend(ba, bb, ratio, 1)
_ = xproto.ChangeGC(X, cid, xproto.GcForeground,
[]uint32{0xff000000 | rR<<16 | gG<<8 | bB})
_ = xproto.PolyLine(X, xproto.CoordModeOrigin, xproto.Drawable(wid),
cid, []xproto.Point{
{X: int16(w / 2), Y: int16(y)},
{X: int16(w - 50), Y: int16(y)},
})
}
}
for {
ev, xerr := X.WaitForEvent()
if xerr != nil {
log.Printf("Error: %s\n", xerr)
return
}
if ev == nil {
return
}
log.Printf("Event: %s\n", ev)
switch e := ev.(type) {
case xproto.UnmapNotifyEvent:
return
case xproto.ConfigureNotifyEvent:
w, h = e.Width, e.Height
case xproto.KeyPressEvent:
start = rand.Uint32() & 0xffffff
end = rand.Uint32() & 0xffffff
gradient()
case xproto.ExposeEvent:
gradient()
}
}
}

397
prototypes/xgb-xrender.go Normal file
View File

@ -0,0 +1,397 @@
package main
// We could also easily use x/image/font/basicfont to load some glyphs into X,
// relying on the fact that it is a vertical array of A8 masks. Though it only
// supports ASCII and has but one size. Best just make a custom BDF loader,
// those fonts have larger coverages and we would be in control. Though again,
// they don't seem to be capable of antialiasing.
import (
"fmt"
"github.com/BurntSushi/xgb"
"github.com/BurntSushi/xgb/render"
"github.com/BurntSushi/xgb/xproto"
// golang.org/x/image/font/opentype cannot render yet but the API is
// more or less the same.
"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"
"log"
"math/rand"
)
func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
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 }
// TODO: We actually need a higher-level function that also keeps track of
// and loads glyphs into the X server.
// TODO: We also need a way to use kerning tables with this, inserting/removing
// advance pixels between neighboring characters.
// 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)
}
}
func main() {
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)
var visual xproto.Visualid
var depth byte
for _, i := range screen.AllowedDepths {
if i.Depth == 32 {
// TODO: Could/should check other parameters.
for _, v := range i.Visuals {
if v.Class == xproto.VisualClassTrueColor {
visual = v.VisualId
depth = i.Depth
break
}
}
}
}
if visual == 0 {
log.Fatalln("cannot find an RGBA TrueColor visual")
}
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.CwBorderPixel|xproto.CwColormap,
[]uint32{0, uint32(mid)})
// This could be included in CreateWindow parameters.
_ = xproto.ChangeWindowAttributes(X, wid,
xproto.CwBackPixel|xproto.CwEventMask, []uint32{0x80808080,
xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
xproto.EventMaskExposure})
title := []byte("Gradient")
_ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
xproto.AtomString, 8, uint32(len(title)), title)
_ = xproto.MapWindow(X, wid)
/*
rfilters, err := render.QueryFilters(X, xproto.Drawable(wid)).Reply()
if err != nil {
log.Fatalln(err)
}
filters := []string{}
for _, f := range rfilters.Filters {
filters = append(filters, f.Name)
}
log.Printf("filters: %v\n", filters)
*/
pformats, err := render.QueryPictFormats(X).Reply()
if err != nil {
log.Fatalln(err)
}
/*
for _, pf := range pformats.Formats {
log.Printf("format %2d: depth %2d, RGBA %3x %3x %3x %3x\n",
pf.Id, pf.Depth,
pf.Direct.RedMask, pf.Direct.GreenMask, pf.Direct.BlueMask,
pf.Direct.AlphaMask)
}
*/
// 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
}
}
}
// ...or just scan through pformats.Formats and look for matches, which is
// what XRenderFindStandardFormat in Xlib does as well as exp/shiny.
f, err := freetype.ParseFont(goregular.TTF)
if err != nil {
log.Fatalln(err)
}
// LCD subpixel rendering isn't supported. :(
opts := &truetype.Options{
Size: 10,
DPI: 96, // TODO: Take this from the screen or monitor.
Hinting: font.HintingFull,
}
face := truetype.NewFace(f, opts)
bounds := f.Bounds(fixed.Int26_6(opts.Size * float64(opts.DPI) *
(64.0 / 72.0)))
var rgbFormat render.Pictformat
for _, pf := range pformats.Formats {
// Hopefully. Might want to check byte order.
if pf.Depth == 32 && pf.Direct.AlphaMask != 0 {
rgbFormat = pf.Id
break
}
}
gsid, err := render.NewGlyphsetId(X)
if err != nil {
log.Fatalln(err)
}
// NOTE: A depth of 24 will not work, the server always rejects it.
// Composite alpha doesn't make sense since golang/freetype can't use it.
// We use RGBA here just so that lines are padded to 32 bits.
_ = render.CreateGlyphSet(X, gsid, rgbFormat)
// NOTE: We could do gamma post-correction in higher precision if we
// implemented our own clone of the image.Image implementation.
nrgb := image.NewRGBA(image.Rect(
+bounds.Min.X.Floor(),
-bounds.Min.Y.Floor(),
+bounds.Max.X.Ceil(),
-bounds.Max.Y.Ceil(),
))
for r := rune(32); r < 128; r++ {
dr, mask, maskp, advance, ok := face.Glyph(
fixed.P(0, 0) /* subpixel destination location */, r)
if !ok {
log.Println("skip")
continue
}
for i := 0; i < len(nrgb.Pix); i++ {
nrgb.Pix[i] = 0
}
draw.Draw(nrgb, dr, mask, maskp, draw.Src)
_ = render.AddGlyphs(X, gsid, 1, []uint32{uint32(r)},
[]render.Glyphinfo{{
Width: uint16(nrgb.Rect.Size().X),
Height: uint16(nrgb.Rect.Size().Y),
X: int16(-bounds.Min.X.Floor()),
Y: int16(+bounds.Max.Y.Ceil()),
XOff: int16(advance.Ceil()),
YOff: int16(0),
}}, []byte(nrgb.Pix))
}
pid, err := render.NewPictureId(X)
if err != nil {
log.Fatalln(err)
}
// Dithering is not supported. :(
render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
// Reserve an ID for the gradient.
gid, err := render.NewPictureId(X)
if err != nil {
log.Fatalln(err)
}
whiteid, err := render.NewPictureId(X)
if err != nil {
log.Fatalln(err)
}
_ = render.CreateSolidFill(X, whiteid, render.Color{
Red: 0xffff,
Green: 0xffff,
Blue: 0xffff,
Alpha: 0xffff,
})
var from, to render.Color
var start, end uint32
recolor := func() {
start = rand.Uint32() & 0xffffff
from = render.Color{
Red: 0x101 * uint16((start>>16)&0xff),
Green: 0x101 * uint16((start>>8)&0xff),
Blue: 0x101 * uint16(start&0xff),
Alpha: 0xffff,
}
end = rand.Uint32() & 0xffffff
to = render.Color{
Red: 0x101 * uint16((end>>16)&0xff),
Green: 0x101 * uint16((end>>8)&0xff),
Blue: 0x101 * uint16(end&0xff),
Alpha: 0xffff,
}
}
var w, h uint16
gradient := func() {
if w < 100 || h < 100 {
return
}
// We could also use a transformation matrix for changes in size.
_ = render.CreateLinearGradient(X, gid,
render.Pointfix{F64ToFixed(0), F64ToFixed(0)},
render.Pointfix{F64ToFixed(0), F64ToFixed(float64(h) - 100)},
2, []render.Fixed{F64ToFixed(0), F64ToFixed(1)},
[]render.Color{from, to})
_ = render.Composite(X, render.PictOpSrc, gid, render.PictureNone, pid,
0, 0, 0, 0, 50, 50, w-100, h-100)
_ = render.FreePicture(X, gid)
_ = compositeString(X, render.PictOpOver, whiteid, pid,
0 /* TODO: mask Pictureformat? */, gsid, 0, 0, 100, 100,
fmt.Sprintf("%#06x - %#06x", start, end))
_ = compositeString(X, render.PictOpOver, whiteid, pid,
0 /* TODO: mask Pictureformat? */, gsid, 0, 0, 100, 150,
"The quick brown fox jumps over the lazy dog.")
}
for {
ev, xerr := X.WaitForEvent()
if xerr != nil {
log.Printf("Error: %s\n", xerr)
return
}
if ev == nil {
return
}
log.Printf("Event: %s\n", ev)
switch e := ev.(type) {
case xproto.UnmapNotifyEvent:
return
case xproto.ConfigureNotifyEvent:
w, h = e.Width, e.Height
recolor()
case xproto.KeyPressEvent:
recolor()
gradient()
case xproto.ExposeEvent:
gradient()
}
}
}