Testing ground for GUI

xgb-draw.go 8.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. // Network-friendly drawing application based on XRender.
  2. //
  3. // TODO: Maybe keep the pixmap as large as the window.
  4. package main
  5. import (
  6. "github.com/BurntSushi/xgb"
  7. "github.com/BurntSushi/xgb/render"
  8. "github.com/BurntSushi/xgb/xproto"
  9. "log"
  10. )
  11. func F64ToFixed(f float64) render.Fixed { return render.Fixed(f * 65536) }
  12. func FixedToF64(f render.Fixed) float64 { return float64(f) / 65536 }
  13. func findPictureFormat(formats []render.Pictforminfo,
  14. depth byte, direct render.Directformat) render.Pictformat {
  15. for _, pf := range formats {
  16. if pf.Depth == depth && pf.Direct == direct {
  17. return pf.Id
  18. }
  19. }
  20. return 0
  21. }
  22. func createNewPicture(X *xgb.Conn, depth byte, drawable xproto.Drawable,
  23. width uint16, height uint16, format render.Pictformat) render.Picture {
  24. pixmapid, err := xproto.NewPixmapId(X)
  25. if err != nil {
  26. log.Fatalln(err)
  27. }
  28. _ = xproto.CreatePixmap(X, depth, pixmapid, drawable, width, height)
  29. pictid, err := render.NewPictureId(X)
  30. if err != nil {
  31. log.Fatalln(err)
  32. }
  33. _ = render.CreatePicture(X, pictid, xproto.Drawable(pixmapid), format,
  34. 0, []uint32{})
  35. return pictid
  36. }
  37. func main() {
  38. X, err := xgb.NewConn()
  39. if err != nil {
  40. log.Fatalln(err)
  41. }
  42. if err := render.Init(X); err != nil {
  43. log.Fatalln(err)
  44. }
  45. setup := xproto.Setup(X)
  46. screen := setup.DefaultScreen(X)
  47. visual, depth := screen.RootVisual, screen.RootDepth
  48. if depth < 24 {
  49. log.Fatalln("need more colors")
  50. }
  51. wid, err := xproto.NewWindowId(X)
  52. if err != nil {
  53. log.Fatalln(err)
  54. }
  55. _ = xproto.CreateWindow(X, depth, wid, screen.Root,
  56. 0, 0, 500, 500, 0, xproto.WindowClassInputOutput,
  57. visual, xproto.CwBackPixel|xproto.CwEventMask,
  58. []uint32{0xffffffff, xproto.EventMaskButtonPress |
  59. xproto.EventMaskButtonMotion | xproto.EventMaskButtonRelease |
  60. xproto.EventMaskStructureNotify | xproto.EventMaskExposure})
  61. title := []byte("Draw")
  62. _ = xproto.ChangeProperty(X, xproto.PropModeReplace, wid, xproto.AtomWmName,
  63. xproto.AtomString, 8, uint32(len(title)), title)
  64. _ = xproto.MapWindow(X, wid)
  65. pformats, err := render.QueryPictFormats(X).Reply()
  66. if err != nil {
  67. log.Fatalln(err)
  68. }
  69. // Find appropriate picture formats.
  70. var pformat, pformatAlpha, pformatRGB render.Pictformat
  71. for _, pd := range pformats.Screens[X.DefaultScreen].Depths {
  72. for _, pv := range pd.Visuals {
  73. if pv.Visual == visual {
  74. pformat = pv.Format
  75. }
  76. }
  77. }
  78. if pformatAlpha = findPictureFormat(pformats.Formats, 8,
  79. render.Directformat{
  80. AlphaShift: 0,
  81. AlphaMask: 0xff,
  82. }); pformat == 0 {
  83. log.Fatalln("required picture format not found")
  84. }
  85. if pformatRGB = findPictureFormat(pformats.Formats, 24,
  86. render.Directformat{
  87. RedShift: 16,
  88. RedMask: 0xff,
  89. GreenShift: 8,
  90. GreenMask: 0xff,
  91. BlueShift: 0,
  92. BlueMask: 0xff,
  93. }); pformatRGB == 0 {
  94. log.Fatalln("required picture format not found")
  95. }
  96. // Picture for the window.
  97. pid, err := render.NewPictureId(X)
  98. if err != nil {
  99. log.Fatalln(err)
  100. }
  101. render.CreatePicture(X, pid, xproto.Drawable(wid), pformat, 0, []uint32{})
  102. // Brush shape.
  103. const brushRadius = 5
  104. brushid, err := render.NewPictureId(X)
  105. if err != nil {
  106. log.Fatalln(err)
  107. }
  108. cFull := render.Color{0xffff, 0xffff, 0xffff, 0xffff}
  109. cTrans := render.Color{0xffff, 0xffff, 0xffff, 0}
  110. _ = render.CreateRadialGradient(X, brushid,
  111. render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)},
  112. render.Pointfix{F64ToFixed(brushRadius), F64ToFixed(brushRadius)},
  113. F64ToFixed(0),
  114. F64ToFixed(brushRadius),
  115. 3, []render.Fixed{F64ToFixed(0), F64ToFixed(0.1), F64ToFixed(1)},
  116. []render.Color{cFull, cFull, cTrans})
  117. // Brush color.
  118. colorid, err := render.NewPictureId(X)
  119. if err != nil {
  120. log.Fatalln(err)
  121. }
  122. _ = render.CreateSolidFill(X, colorid, render.Color{
  123. Red: 0x4444,
  124. Green: 0x8888,
  125. Blue: 0xffff,
  126. Alpha: 0xffff,
  127. })
  128. // Various pixmaps.
  129. const (
  130. pixWidth = 1000
  131. pixHeight = 1000
  132. )
  133. canvasid := createNewPicture(X, 24,
  134. xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB)
  135. bufferid := createNewPicture(X, 24,
  136. xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatRGB)
  137. maskid := createNewPicture(X, 8,
  138. xproto.Drawable(screen.Root), pixWidth, pixHeight, pformatAlpha)
  139. // Smoothing by way of blur, apparently a misguided idea.
  140. /*
  141. _ = render.SetPictureFilter(X, maskid,
  142. uint16(len("convolution")), "convolution",
  143. []render.Fixed{F64ToFixed(3), F64ToFixed(3),
  144. F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0),
  145. F64ToFixed(0.15), F64ToFixed(0.40), F64ToFixed(0.15),
  146. F64ToFixed(0), F64ToFixed(0.15), F64ToFixed(0)})
  147. */
  148. // Pixmaps come uninitialized.
  149. _ = render.FillRectangles(X,
  150. render.PictOpSrc, canvasid, render.Color{
  151. Red: 0xffff, Green: 0xffff, Blue: 0xffff, Alpha: 0xffff,
  152. }, []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}})
  153. // This is the only method we can use to render brush strokes without
  154. // alpha accumulation due to stamping. Though this also seems to be
  155. // misguided. Keeping it here for educational purposes.
  156. //
  157. // ConjointOver is defined as: A = Aa * 1 + Ab * max(1-Aa/Ab,0)
  158. // which basically resolves to: A = max(Aa, Ab)
  159. // which equals "lighten" with one channel only.
  160. //
  161. // Resources:
  162. // - https://www.cairographics.org/operators/
  163. // - http://ssp.impulsetrain.com/porterduff.html
  164. // - https://keithp.com/~keithp/talks/renderproblems/renderproblems/render-title.html
  165. // - https://keithp.com/~keithp/talks/cairo2003.pdf
  166. drawPointAt := func(x, y int16) {
  167. _ = render.Composite(X, render.PictOpConjointOver,
  168. brushid, render.PictureNone, maskid,
  169. 0, 0, 0, 0, x-brushRadius, y-brushRadius,
  170. brushRadius*2, brushRadius*2)
  171. _ = render.SetPictureClipRectangles(X, bufferid,
  172. x-brushRadius, y-brushRadius, []xproto.Rectangle{
  173. {Width: brushRadius * 2, Height: brushRadius * 2}})
  174. _ = render.Composite(X, render.PictOpSrc,
  175. canvasid, render.PictureNone, bufferid,
  176. 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
  177. pixWidth, pixHeight)
  178. _ = render.Composite(X, render.PictOpOver,
  179. colorid, maskid, bufferid,
  180. 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
  181. pixWidth, pixHeight)
  182. // Composited, now blit to window without flicker.
  183. _ = render.SetPictureClipRectangles(X, pid,
  184. x-brushRadius, y-brushRadius, []xproto.Rectangle{
  185. {Width: brushRadius * 2, Height: brushRadius * 2}})
  186. _ = render.Composite(X, render.PictOpSrc,
  187. bufferid, render.PictureNone, pid,
  188. 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
  189. pixWidth, pixHeight)
  190. }
  191. // Integer version of Bresenham's line drawing algorithm
  192. drawLine := func(x0, y0, x1, y1 int16) {
  193. dx, dy := x1-x0, y1-y0
  194. if dx < 0 {
  195. dx = -dx
  196. }
  197. if dy < 0 {
  198. dy = -dy
  199. }
  200. steep := dx < dy
  201. if steep {
  202. // Flip the coordinate system on input
  203. x0, y0 = y0, x0
  204. x1, y1 = y1, x1
  205. dx, dy = dy, dx
  206. }
  207. var stepX, stepY int16 = 1, 1
  208. if x0 > x1 {
  209. stepX = -1
  210. }
  211. if y0 > y1 {
  212. stepY = -1
  213. }
  214. dpr := dy * 2
  215. delta := dpr - dx
  216. dpru := delta - dx
  217. for ; dx > 0; dx-- {
  218. // Unflip the coordinate system on output
  219. if steep {
  220. drawPointAt(y0, x0)
  221. } else {
  222. drawPointAt(x0, y0)
  223. }
  224. x0 += stepX
  225. if delta > 0 {
  226. y0 += stepY
  227. delta += dpru
  228. } else {
  229. delta += dpr
  230. }
  231. }
  232. }
  233. var startX, startY int16 = 0, 0
  234. drawing := false
  235. for {
  236. ev, xerr := X.WaitForEvent()
  237. if xerr != nil {
  238. log.Printf("Error: %s\n", xerr)
  239. return
  240. }
  241. if ev == nil {
  242. return
  243. }
  244. log.Printf("Event: %s\n", ev)
  245. switch e := ev.(type) {
  246. case xproto.UnmapNotifyEvent:
  247. return
  248. case xproto.ExposeEvent:
  249. _ = render.SetPictureClipRectangles(X, pid, int16(e.X), int16(e.Y),
  250. []xproto.Rectangle{{Width: e.Width, Height: e.Height}})
  251. // Not bothering to deflicker here using the buffer pixmap,
  252. // with compositing this event is rare enough.
  253. _ = render.Composite(X, render.PictOpSrc,
  254. canvasid, render.PictureNone, pid,
  255. 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
  256. pixWidth, pixHeight)
  257. if drawing {
  258. _ = render.Composite(X, render.PictOpOver,
  259. colorid, maskid, pid,
  260. 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
  261. pixWidth, pixHeight)
  262. }
  263. case xproto.ButtonPressEvent:
  264. if e.Detail == xproto.ButtonIndex1 {
  265. render.FillRectangles(X,
  266. render.PictOpSrc, maskid, render.Color{},
  267. []xproto.Rectangle{{Width: pixWidth, Height: pixHeight}})
  268. drawing = true
  269. drawPointAt(e.EventX, e.EventY)
  270. startX, startY = e.EventX, e.EventY
  271. }
  272. case xproto.MotionNotifyEvent:
  273. if drawing {
  274. drawLine(startX, startY, e.EventX, e.EventY)
  275. startX, startY = e.EventX, e.EventY
  276. }
  277. case xproto.ButtonReleaseEvent:
  278. if e.Detail == xproto.ButtonIndex1 {
  279. _ = render.Composite(X, render.PictOpOver,
  280. colorid, maskid, canvasid,
  281. 0, 0, 0, 0, 0 /* dst-x */, 0, /* dst-y */
  282. pixWidth, pixHeight)
  283. drawing = false
  284. }
  285. }
  286. }
  287. }