Testing ground for GUI

xgb-text-viewer.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. // This is an amalgamation of xgb-xrender.go and xgb-keys.go and more of a demo,
  2. // some comments have been stripped.
  3. package main
  4. import (
  5. "github.com/BurntSushi/xgb"
  6. "github.com/BurntSushi/xgb/render"
  7. "github.com/BurntSushi/xgb/xproto"
  8. "github.com/golang/freetype"
  9. "github.com/golang/freetype/truetype"
  10. "golang.org/x/image/font"
  11. "golang.org/x/image/font/gofont/goregular"
  12. "golang.org/x/image/math/fixed"
  13. "image"
  14. "image/draw"
  15. "io/ioutil"
  16. "log"
  17. "os"
  18. "strings"
  19. )
  20. func glyphListBytes(buf []byte, runes []rune, size int) int {
  21. b := 0
  22. for _, r := range runes {
  23. switch size {
  24. default:
  25. buf[b] = byte(r)
  26. b += 1
  27. case 2:
  28. xgb.Put16(buf[b:], uint16(r))
  29. b += 2
  30. case 4:
  31. xgb.Put32(buf[b:], uint32(r))
  32. b += 4
  33. }
  34. }
  35. return xgb.Pad(b)
  36. }
  37. // When the len is 255, a GLYPHABLE follows, otherwise a list of CARD8/16/32.
  38. func glyphEltHeaderBytes(buf []byte, len byte, deltaX, deltaY int16) int {
  39. b := 0
  40. buf[b] = len
  41. b += 4
  42. xgb.Put16(buf[b:], uint16(deltaX))
  43. b += 2
  44. xgb.Put16(buf[b:], uint16(deltaY))
  45. b += 2
  46. return xgb.Pad(b)
  47. }
  48. type xgbCookie interface{ Check() error }
  49. // compositeString makes an appropriate render.CompositeGlyphs request,
  50. // assuming that glyphs equal Unicode codepoints.
  51. func compositeString(c *xgb.Conn, op byte, src, dst render.Picture,
  52. maskFormat render.Pictformat, glyphset render.Glyphset, srcX, srcY int16,
  53. destX, destY int16, text string) xgbCookie {
  54. runes := []rune(text)
  55. var highest rune
  56. for _, r := range runes {
  57. if r > highest {
  58. highest = r
  59. }
  60. }
  61. size := 1
  62. switch {
  63. case highest > 1<<16:
  64. size = 4
  65. case highest > 1<<8:
  66. size = 2
  67. }
  68. // They gave up on the XCB protocol API and we need to serialize explicitly.
  69. // To spare us from caring about the padding, use the largest number lesser
  70. // than 255 that is divisible by 4 (for size 2 and 4 the requirements are
  71. // less strict but this works in the general case).
  72. const maxPerChunk = 252
  73. buf := make([]byte, (len(runes)+maxPerChunk-1)/maxPerChunk*8+len(runes)*size)
  74. b := 0
  75. for len(runes) > maxPerChunk {
  76. b += glyphEltHeaderBytes(buf[b:], maxPerChunk, 0, 0)
  77. b += glyphListBytes(buf[b:], runes[:maxPerChunk], size)
  78. runes = runes[maxPerChunk:]
  79. }
  80. if len(runes) > 0 {
  81. b += glyphEltHeaderBytes(buf[b:], byte(len(runes)), destX, destY)
  82. b += glyphListBytes(buf[b:], runes, size)
  83. }
  84. switch size {
  85. default:
  86. return render.CompositeGlyphs8(c, op, src, dst, maskFormat, glyphset,
  87. srcX, srcY, buf)
  88. case 2:
  89. return render.CompositeGlyphs16(c, op, src, dst, maskFormat, glyphset,
  90. srcX, srcY, buf)
  91. case 4:
  92. return render.CompositeGlyphs32(c, op, src, dst, maskFormat, glyphset,
  93. srcX, srcY, buf)
  94. }
  95. }
  96. type textRenderer struct {
  97. f *truetype.Font
  98. opts *truetype.Options
  99. face font.Face
  100. bounds fixed.Rectangle26_6 // outer bounds for all the font's glyph
  101. buf *image.RGBA // rendering buffer
  102. X *xgb.Conn
  103. gsid render.Glyphset
  104. loaded map[rune]bool
  105. }
  106. func newTextRenderer(X *xgb.Conn, ttf []byte, opts *truetype.Options) (
  107. *textRenderer, error) {
  108. pformats, err := render.QueryPictFormats(X).Reply()
  109. if err != nil {
  110. return nil, err
  111. }
  112. // We use RGBA here just so that lines are padded to 32 bits.
  113. // Since there's no subpixel antialiasing and alpha is premultiplied,
  114. // it doesn't even mater that RGBA is interpreted as ARGB or BGRA.
  115. var rgbFormat render.Pictformat
  116. for _, pf := range pformats.Formats {
  117. if pf.Depth == 32 && pf.Direct.AlphaMask != 0 {
  118. rgbFormat = pf.Id
  119. break
  120. }
  121. }
  122. tr := &textRenderer{opts: opts, X: X, loaded: make(map[rune]bool)}
  123. if tr.f, err = freetype.ParseFont(goregular.TTF); err != nil {
  124. return nil, err
  125. }
  126. tr.face = truetype.NewFace(tr.f, opts)
  127. tr.bounds = tr.f.Bounds(fixed.Int26_6(opts.Size * float64(opts.DPI) *
  128. (64.0 / 72.0)))
  129. if tr.gsid, err = render.NewGlyphsetId(X); err != nil {
  130. return nil, err
  131. }
  132. if err := render.CreateGlyphSetChecked(X, tr.gsid, rgbFormat).
  133. Check(); err != nil {
  134. return nil, err
  135. }
  136. tr.buf = image.NewRGBA(image.Rect(
  137. +tr.bounds.Min.X.Floor(),
  138. -tr.bounds.Min.Y.Floor(),
  139. +tr.bounds.Max.X.Ceil(),
  140. -tr.bounds.Max.Y.Ceil(),
  141. ))
  142. return tr, nil
  143. }
  144. func (tr *textRenderer) addRune(r rune) bool {
  145. dr, mask, maskp, advance, ok := tr.face.Glyph(
  146. fixed.P(0, 0) /* subpixel destination location */, r)
  147. if !ok {
  148. return false
  149. }
  150. for i := 0; i < len(tr.buf.Pix); i++ {
  151. tr.buf.Pix[i] = 0
  152. }
  153. // Copying, since there are absolutely no guarantees.
  154. draw.Draw(tr.buf, dr, mask, maskp, draw.Src)
  155. _ = render.AddGlyphs(tr.X, tr.gsid, 1, []uint32{uint32(r)},
  156. []render.Glyphinfo{{
  157. Width: uint16(tr.buf.Rect.Size().X),
  158. Height: uint16(tr.buf.Rect.Size().Y),
  159. X: int16(-tr.bounds.Min.X.Floor()),
  160. Y: int16(+tr.bounds.Max.Y.Ceil()),
  161. XOff: int16(advance.Ceil()),
  162. YOff: int16(0),
  163. }}, []byte(tr.buf.Pix))
  164. return true
  165. }
  166. func (tr *textRenderer) render(src, dst render.Picture,
  167. srcX, srcY, destX, destY int16, text string) xgbCookie {
  168. // XXX: You're really supposed to handle tabs differently from this.
  169. text = strings.Replace(text, "\t", " ", -1)
  170. for _, r := range text {
  171. if !tr.loaded[r] {
  172. tr.addRune(r)
  173. tr.loaded[r] = true
  174. }
  175. }
  176. return compositeString(tr.X, render.PictOpOver, src, dst,
  177. 0 /* TODO: mask Pictureformat? */, tr.gsid,
  178. srcX, srcY, destX, destY, text)
  179. }
  180. const (
  181. ksEscape = 0xff1b
  182. ksUp = 0xff52
  183. ksDown = 0xff54
  184. ksPageUp = 0xff55
  185. ksPageDown = 0xff56
  186. ksModeSwitch = 0xff7e
  187. )
  188. type keyMapper struct {
  189. X *xgb.Conn
  190. setup *xproto.SetupInfo
  191. mapping *xproto.GetKeyboardMappingReply
  192. modeSwitchMask uint16
  193. }
  194. func newKeyMapper(X *xgb.Conn) (*keyMapper, error) {
  195. m := &keyMapper{X: X, setup: xproto.Setup(X)}
  196. if err := m.update(); err != nil {
  197. return nil, err
  198. }
  199. return m, nil
  200. }
  201. func (km *keyMapper) update() error {
  202. var err error
  203. km.mapping, err = xproto.GetKeyboardMapping(km.X, km.setup.MinKeycode,
  204. byte(km.setup.MaxKeycode-km.setup.MinKeycode+1)).Reply()
  205. if err != nil {
  206. return err
  207. }
  208. km.modeSwitchMask = 0
  209. // The order is "Shift, Lock, Control, Mod1, Mod2, Mod3, Mod4, and Mod5."
  210. mm, err := xproto.GetModifierMapping(km.X).Reply()
  211. if err != nil {
  212. return err
  213. }
  214. perMod := int(mm.KeycodesPerModifier)
  215. perKc := int(km.mapping.KeysymsPerKeycode)
  216. for mod := 0; mod < 8; mod++ {
  217. for _, kc := range mm.Keycodes[mod*perMod : (mod+1)*perMod] {
  218. if kc == 0 {
  219. continue
  220. }
  221. k := int(kc - km.setup.MinKeycode)
  222. for _, ks := range km.mapping.Keysyms[k*perKc : (k+1)*perKc] {
  223. if ks == ksModeSwitch {
  224. km.modeSwitchMask |= 1 << uint(mod)
  225. }
  226. }
  227. }
  228. }
  229. return nil
  230. }
  231. func (km *keyMapper) decode(e xproto.KeyPressEvent) (result xproto.Keysym) {
  232. step := int(km.mapping.KeysymsPerKeycode)
  233. from := int(e.Detail-km.setup.MinKeycode) * step
  234. ks := km.mapping.Keysyms[from : from+step]
  235. // Strip trailing NoSymbol entries.
  236. for len(ks) > 0 && ks[len(ks)-1] == 0 {
  237. ks = ks[:len(ks)-1]
  238. }
  239. // Expand back to at least 4.
  240. switch {
  241. case len(ks) == 1:
  242. ks = append(ks, 0, ks[0], 0)
  243. case len(ks) == 2:
  244. ks = append(ks, ks[0], ks[1])
  245. case len(ks) == 3:
  246. ks = append(ks, 0)
  247. }
  248. // Other silly expansion rules, only applied to basic ASCII since we
  249. // don't have translation tables to Unicode here for brevity.
  250. if ks[1] == 0 {
  251. ks[1] = ks[0]
  252. if ks[0] >= 'A' && ks[0] <= 'Z' ||
  253. ks[0] >= 'a' && ks[0] <= 'z' {
  254. ks[0] = ks[0] | 32
  255. ks[1] = ks[0] &^ 32
  256. }
  257. }
  258. if ks[3] == 0 {
  259. ks[3] = ks[2]
  260. if ks[2] >= 'A' && ks[2] <= 'Z' ||
  261. ks[2] >= 'a' && ks[2] <= 'z' {
  262. ks[2] = ks[2] | 32
  263. ks[3] = ks[2] &^ 32
  264. }
  265. }
  266. offset := 0
  267. if e.State&km.modeSwitchMask != 0 {
  268. offset += 2
  269. }
  270. shift := e.State&xproto.ModMaskShift != 0
  271. lock := e.State&xproto.ModMaskLock != 0
  272. switch {
  273. case !shift && !lock:
  274. result = ks[offset+0]
  275. case !shift && lock:
  276. if ks[offset+0] >= 'a' && ks[offset+0] <= 'z' {
  277. result = ks[offset+1]
  278. } else {
  279. result = ks[offset+0]
  280. }
  281. case shift && lock:
  282. if ks[offset+1] >= 'a' && ks[offset+1] <= 'z' {
  283. result = ks[offset+1] &^ 32
  284. } else {
  285. result = ks[offset+1]
  286. }
  287. case shift:
  288. result = ks[offset+1]
  289. }
  290. return
  291. }
  292. func main() {
  293. if len(os.Args) < 2 {
  294. log.Fatalln("no filename given")
  295. }
  296. text, err := ioutil.ReadFile(os.Args[1])
  297. if err != nil {
  298. log.Fatalln(err)
  299. }
  300. lines := strings.Split(string(text), "\n")
  301. X, err := xgb.NewConn()
  302. if err != nil {
  303. log.Fatalln(err)
  304. }
  305. if err := render.Init(X); err != nil {
  306. log.Fatalln(err)
  307. }
  308. setup := xproto.Setup(X)
  309. screen := setup.DefaultScreen(X)
  310. visual, depth := screen.RootVisual, screen.RootDepth
  311. // TODO: We should check that we find it, though we don't /need/ alpha here,
  312. // it's just a minor improvement--affects the backpixel value.
  313. for _, i := range screen.AllowedDepths {
  314. // TODO: Could/should check other parameters.
  315. for _, v := range i.Visuals {
  316. if i.Depth == 32 && v.Class == xproto.VisualClassTrueColor {
  317. visual, depth = v.VisualId, i.Depth
  318. break
  319. }
  320. }
  321. }
  322. mid, err := xproto.NewColormapId(X)
  323. if err != nil {
  324. log.Fatalln(err)
  325. }
  326. _ = xproto.CreateColormap(
  327. X, xproto.ColormapAllocNone, mid, screen.Root, visual)
  328. wid, err := xproto.NewWindowId(X)
  329. if err != nil {
  330. log.Fatalln(err)
  331. }
  332. // Border pixel and colormap are required when depth differs from parent.
  333. _ = xproto.CreateWindow(X, depth, wid, screen.Root,
  334. 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
  335. visual, xproto.CwBackPixel|xproto.CwBorderPixel|xproto.CwEventMask|
  336. xproto.CwColormap, []uint32{0xf0f0f0f0, 0,
  337. xproto.EventMaskStructureNotify | xproto.EventMaskKeyPress |
  338. /* KeymapNotify */ xproto.EventMaskKeymapState |
  339. xproto.EventMaskExposure | xproto.EventMaskButtonPress,
  340. uint32(mid)})
  341. title := []byte("Viewer")
  342. _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
  343. xproto.AtomString, 8, uint32(len(title)), title)
  344. _ = xproto.MapWindow(X, wid)
  345. pformats, err := render.QueryPictFormats(X).Reply()
  346. if err != nil {
  347. log.Fatalln(err)
  348. }
  349. // Similar to XRenderFindVisualFormat.
  350. // The DefaultScreen is almost certain to be zero.
  351. var pformat render.Pictformat
  352. for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
  353. // This check seems to be slightly extraneous.
  354. if pd.Depth != depth {
  355. continue
  356. }
  357. for _, pv := range pd.Visuals {
  358. if pv.Visual == visual {
  359. pformat = pv.Format
  360. }
  361. }
  362. }
  363. pid, err := render.NewPictureId(X)
  364. if err != nil {
  365. log.Fatalln(err)
  366. }
  367. render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
  368. blackid, err := render.NewPictureId(X)
  369. if err != nil {
  370. log.Fatalln(err)
  371. }
  372. _ = render.CreateSolidFill(X, blackid, render.Color{Alpha: 0xffff})
  373. tr, err := newTextRenderer(X, goregular.TTF, &truetype.Options{
  374. Size: 10,
  375. DPI: float64(screen.WidthInPixels) /
  376. float64(screen.WidthInMillimeters) * 25.4,
  377. Hinting: font.HintingFull,
  378. })
  379. if err != nil {
  380. log.Fatalln(err)
  381. }
  382. scroll := 0 // index of the top line
  383. var w, h uint16
  384. redraw := func() {
  385. y, ascent, step := 5, tr.bounds.Max.Y.Ceil(),
  386. tr.bounds.Max.Y.Ceil()-tr.bounds.Min.Y.Floor()
  387. for _, line := range lines[scroll:] {
  388. if uint16(y) >= h {
  389. break
  390. }
  391. _ = tr.render(blackid, pid, 0, 0, 5, int16(y+ascent), line)
  392. y += step
  393. }
  394. vis := float64(h-10) / float64(step)
  395. if vis < float64(len(lines)) {
  396. length := float64(step) * (vis + 1) * vis / float64(len(lines))
  397. start := float64(step) * float64(scroll) * vis / float64(len(lines))
  398. _ = render.FillRectangles(X, render.PictOpSrc, pid,
  399. render.Color{Alpha: 0xffff}, []xproto.Rectangle{{
  400. X: int16(w - 15), Y: int16(start),
  401. Width: 15, Height: uint16(length + 10)}})
  402. }
  403. }
  404. km, err := newKeyMapper(X)
  405. if err != nil {
  406. log.Fatalln(err)
  407. }
  408. for {
  409. ev, xerr := X.WaitForEvent()
  410. if xerr != nil {
  411. log.Printf("Error: %s\n", xerr)
  412. return
  413. }
  414. if ev == nil {
  415. return
  416. }
  417. switch e := ev.(type) {
  418. case xproto.UnmapNotifyEvent:
  419. return
  420. case xproto.ConfigureNotifyEvent:
  421. w, h = e.Width, e.Height
  422. case xproto.MappingNotifyEvent:
  423. _ = km.update()
  424. case xproto.KeyPressEvent:
  425. _ = xproto.ClearArea(X, true /* ExposeEvent */, wid, 0, 0, w, h)
  426. const pageJump = 40
  427. switch km.decode(e) {
  428. case ksEscape:
  429. return
  430. case ksUp:
  431. if scroll >= 1 {
  432. scroll--
  433. }
  434. case ksDown:
  435. if scroll+1 < len(lines) {
  436. scroll++
  437. }
  438. case ksPageUp:
  439. if scroll >= pageJump {
  440. scroll -= pageJump
  441. }
  442. case ksPageDown:
  443. if scroll+pageJump < len(lines) {
  444. scroll += pageJump
  445. }
  446. }
  447. case xproto.ButtonPressEvent:
  448. _ = xproto.ClearArea(X, true /* ExposeEvent */, wid, 0, 0, w, h)
  449. switch e.Detail {
  450. case xproto.ButtonIndex4:
  451. if scroll > 0 {
  452. scroll--
  453. }
  454. case xproto.ButtonIndex5:
  455. if scroll+1 < len(lines) {
  456. scroll++
  457. }
  458. }
  459. case xproto.ExposeEvent:
  460. // FIXME: The window's context haven't necessarily been destroyed.
  461. if e.Count == 0 {
  462. redraw()
  463. }
  464. }
  465. }
  466. }