User interface toolkit for Go with support for SDL2 and HTML Canvas render targets.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

337 lines
8.3 KiB

  1. package ui
  2. import (
  3. "git.kirsle.net/go/render"
  4. )
  5. // Pack provides configuration fields for Frame.Pack().
  6. type Pack struct {
  7. // Side of the parent to anchor the position to, like N, SE, W. Default
  8. // is Center.
  9. Side Side
  10. // If the widget is smaller than its allocated space, grow the widget
  11. // to fill its space in the Frame.
  12. Fill bool
  13. FillX bool
  14. FillY bool
  15. Padding int // Equal padding on X and Y.
  16. PadX int
  17. PadY int
  18. Expand bool // Widget should grow its allocated space to better fill the parent.
  19. }
  20. // Pack a widget along a side of the frame.
  21. func (w *Frame) Pack(child Widget, config ...Pack) {
  22. var C Pack
  23. if len(config) > 0 {
  24. C = config[0]
  25. }
  26. // Initialize the pack list for this side?
  27. if _, ok := w.packs[C.Side]; !ok {
  28. w.packs[C.Side] = []packedWidget{}
  29. }
  30. // Padding: if the user only provided Padding add it to both
  31. // the X and Y value. If the user additionally provided the X
  32. // and Y value, it will add to the base padding as you'd expect.
  33. C.PadX += C.Padding
  34. C.PadY += C.Padding
  35. // Fill: true implies both directions.
  36. if C.Fill {
  37. C.FillX = true
  38. C.FillY = true
  39. }
  40. // Adopt the child widget so it can access the Frame.
  41. child.SetParent(w)
  42. w.packs[C.Side] = append(w.packs[C.Side], packedWidget{
  43. widget: child,
  44. pack: C,
  45. })
  46. w.Add(child)
  47. }
  48. // computePacked processes all the Pack layout widgets in the Frame.
  49. func (w *Frame) computePacked(e render.Engine) {
  50. var (
  51. frameSize = w.BoxSize()
  52. // maxWidth and maxHeight are always the computed minimum dimensions
  53. // that the Frame must be to contain all of its children. If the Frame
  54. // was configured with an explicit Size, the Frame will be that Size,
  55. // but we still calculate how much space the widgets _actually_ take
  56. // so we can expand them to fill remaining space in fixed size Frames.
  57. maxWidth int
  58. maxHeight int
  59. visited = []packedWidget{}
  60. expanded = []packedWidget{}
  61. )
  62. // Iterate through all directions and compute how much space to
  63. // reserve to contain all of their widgets.
  64. for side := SideMin; side <= SideMax; side++ {
  65. if _, ok := w.packs[side]; !ok {
  66. continue
  67. }
  68. var (
  69. x int
  70. y int
  71. yDirection = 1
  72. xDirection = 1
  73. )
  74. if side.IsSouth() {
  75. y = frameSize.H - w.BoxThickness(4)
  76. yDirection = -1
  77. } else if side.IsEast() {
  78. x = frameSize.W - w.BoxThickness(4)
  79. xDirection = -1
  80. }
  81. for _, packedWidget := range w.packs[side] {
  82. child := packedWidget.widget
  83. pack := packedWidget.pack
  84. child.Compute(e)
  85. if child.Hidden() {
  86. continue
  87. }
  88. x += pack.PadX * xDirection
  89. y += pack.PadY * yDirection
  90. var (
  91. // point = child.Point()
  92. size = child.Size()
  93. yStep = y * yDirection
  94. xStep = x * xDirection
  95. )
  96. if xStep+size.W+(pack.PadX*2) > maxWidth {
  97. maxWidth = xStep + size.W + (pack.PadX * 2)
  98. }
  99. if yStep+size.H+(pack.PadY*2) > maxHeight {
  100. maxHeight = yStep + size.H + (pack.PadY * 2)
  101. }
  102. if side.IsSouth() {
  103. y -= size.H - pack.PadY
  104. }
  105. if side.IsEast() {
  106. x -= size.W - pack.PadX
  107. }
  108. // NOTE: we place the child's position relative to the Frame's
  109. // position. So a child placed at the top/left of the Frame gets
  110. // an x,y near zero regardless of the Frame's position.
  111. child.MoveTo(render.NewPoint(x, y))
  112. if side.IsNorth() {
  113. y += size.H + pack.PadY
  114. }
  115. if side.IsWest() {
  116. x += size.W + pack.PadX
  117. }
  118. visited = append(visited, packedWidget)
  119. if pack.Expand { // TODO: don't fuck with children of fixed size
  120. expanded = append(expanded, packedWidget)
  121. }
  122. }
  123. }
  124. // If we have extra space in the Frame and any expanding widgets, let the
  125. // expanding widgets grow and share the remaining space.
  126. computedSize := render.NewRect(maxWidth, maxHeight)
  127. if len(expanded) > 0 && !frameSize.IsZero() { // && frameSize.Bigger(computedSize) {
  128. // Divy up the size available.
  129. growBy := render.Rect{
  130. W: ((frameSize.W - computedSize.W) / len(expanded)) - w.BoxThickness(4),
  131. H: ((frameSize.H - computedSize.H) / len(expanded)) - w.BoxThickness(4),
  132. }
  133. for _, pw := range expanded {
  134. // Grow the widget but maintain its auto-size flag, in case the widget
  135. // was not given an explicit size before.
  136. size := pw.widget.Size()
  137. pw.widget.ResizeAuto(render.Rect{
  138. W: size.W + growBy.W,
  139. H: size.H + growBy.H,
  140. })
  141. pw.widget.Compute(e)
  142. }
  143. }
  144. // If we're not using a fixed Frame size, use the dynamically computed one.
  145. if !w.FixedSize() {
  146. frameSize = render.NewRect(maxWidth, maxHeight)
  147. } else {
  148. // If either of the sizes were left zero, use the dynamically computed one.
  149. if frameSize.W == 0 {
  150. frameSize.W = maxWidth
  151. }
  152. if frameSize.H == 0 {
  153. frameSize.H = maxHeight
  154. }
  155. }
  156. // Rescan all the widgets in this side to re-center them
  157. // in their space.
  158. innerFrameSize := render.NewRect(
  159. frameSize.W-w.BoxThickness(2),
  160. frameSize.H-w.BoxThickness(2),
  161. )
  162. for _, pw := range visited {
  163. var (
  164. child = pw.widget
  165. pack = pw.pack
  166. point = child.Point()
  167. size = child.Size()
  168. resize = size
  169. resized bool
  170. moved bool
  171. )
  172. if pack.Side.IsNorth() || pack.Side.IsSouth() {
  173. // Aligned to the top or bottom. If the widget Fills horizontally,
  174. // resize it so its Width matches the frame's Width.
  175. if pack.FillX && resize.W < innerFrameSize.W {
  176. resize.W = innerFrameSize.W - w.BoxThickness(2) // TODO: child.BoxThickness instead??
  177. resized = true
  178. }
  179. // If it does not Fill horizontally and there is extra horizontal
  180. // space, center the widget inside the space. TODO: Anchor option
  181. // could align the widget to the left or right instead of center.
  182. if resize.W < innerFrameSize.W-w.BoxThickness(4) {
  183. if pack.Side.IsCenter() {
  184. point.X = (innerFrameSize.W / 2) - (resize.W / 2)
  185. } else if pack.Side.IsWest() {
  186. point.X = pack.PadX
  187. } else if pack.Side.IsEast() {
  188. point.X = innerFrameSize.W - resize.W - pack.PadX
  189. }
  190. moved = true
  191. }
  192. } else if pack.Side.IsWest() || pack.Side.IsEast() {
  193. // Similar logic to the above, but widget is packed against the
  194. // left or right edge. Handle vertical Fill to grow the widget.
  195. if pack.FillY && resize.H < innerFrameSize.H {
  196. resize.H = innerFrameSize.H - w.BoxThickness(2) // TODO: child.BoxThickness instead??
  197. resized = true
  198. }
  199. // Vertically align the widgets.
  200. if resize.H < innerFrameSize.H {
  201. if pack.Side.IsMiddle() {
  202. point.Y = (innerFrameSize.H / 2) - (resize.H / 2) // - w.BoxThickness(1)
  203. } else if pack.Side.IsNorth() {
  204. point.Y = pack.PadY // - w.BoxThickness(4)
  205. } else if pack.Side.IsSouth() {
  206. point.Y = innerFrameSize.H - resize.H - pack.PadY
  207. }
  208. moved = true
  209. }
  210. } else {
  211. panic("unsupported pack.Side")
  212. }
  213. if resized && size != resize {
  214. child.ResizeAuto(resize)
  215. child.Compute(e)
  216. }
  217. if moved {
  218. child.MoveTo(point)
  219. }
  220. }
  221. // TODO: the Frame should ResizeAuto so it doesn't mark fixedSize=true.
  222. // Currently there's a bug where frames will grow when the window grows but
  223. // never shrink again when the window shrinks.
  224. // if !w.FixedSize() {
  225. w.Resize(render.NewRect(
  226. frameSize.W-w.BoxThickness(2),
  227. frameSize.H-w.BoxThickness(2),
  228. ))
  229. // }
  230. }
  231. // Side is a cardinal direction.
  232. type Side uint8
  233. // Side values.
  234. const (
  235. Center Side = iota
  236. N
  237. NE
  238. E
  239. SE
  240. S
  241. SW
  242. W
  243. NW
  244. )
  245. // Range of Side values.
  246. const (
  247. SideMin = Center
  248. SideMax = NW
  249. )
  250. // IsNorth returns if the side is N, NE or NW.
  251. func (a Side) IsNorth() bool {
  252. return a == N || a == NE || a == NW
  253. }
  254. // IsSouth returns if the side is S, SE or SW.
  255. func (a Side) IsSouth() bool {
  256. return a == S || a == SE || a == SW
  257. }
  258. // IsEast returns if the side is E, NE or SE.
  259. func (a Side) IsEast() bool {
  260. return a == E || a == NE || a == SE
  261. }
  262. // IsWest returns if the side is W, NW or SW.
  263. func (a Side) IsWest() bool {
  264. return a == W || a == NW || a == SW
  265. }
  266. // IsCenter returns if the side is Center, N or S, to determine
  267. // whether to align text as centered for North/South sides.
  268. func (a Side) IsCenter() bool {
  269. return a == Center || a == N || a == S
  270. }
  271. // IsMiddle returns if the side is Center, E or W, to determine
  272. // whether to align text as middled for East/West sides.
  273. func (a Side) IsMiddle() bool {
  274. return a == Center || a == W || a == E
  275. }
  276. type packLayout struct {
  277. widgets []packedWidget
  278. }
  279. type packedWidget struct {
  280. widget Widget
  281. pack Pack
  282. fill uint8
  283. }
  284. // packedWidget.fill values
  285. const (
  286. fillNone uint8 = iota
  287. fillX
  288. fillY
  289. fillBoth
  290. )