layout.go 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. // SPDX-License-Identifier: Unlicense OR MIT
  2. package layout
  3. import (
  4. "image"
  5. "gioui.org/f32"
  6. "gioui.org/op"
  7. "gioui.org/unit"
  8. )
  9. // Constraints represent the minimum and maximum size of a widget.
  10. //
  11. // A widget does not have to treat its constraints as "hard". For
  12. // example, if it's passed a constraint with a minimum size that's
  13. // smaller than its actual minimum size, it should return its minimum
  14. // size dimensions instead. Parent widgets should deal appropriately
  15. // with child widgets that return dimensions that do not fit their
  16. // constraints (for example, by clipping).
  17. type Constraints struct {
  18. Min, Max image.Point
  19. }
  20. // Dimensions are the resolved size and baseline for a widget.
  21. //
  22. // Baseline is the distance from the bottom of a widget to the baseline of
  23. // any text it contains (or 0). The purpose is to be able to align text
  24. // that span multiple widgets.
  25. type Dimensions struct {
  26. Size image.Point
  27. Baseline int
  28. }
  29. // Axis is the Horizontal or Vertical direction.
  30. type Axis uint8
  31. // Alignment is the mutual alignment of a list of widgets.
  32. type Alignment uint8
  33. // Direction is the alignment of widgets relative to a containing
  34. // space.
  35. type Direction uint8
  36. // Widget is a function scope for drawing, processing events and
  37. // computing dimensions for a user interface element.
  38. type Widget func(gtx Context) Dimensions
  39. const (
  40. Start Alignment = iota
  41. End
  42. Middle
  43. Baseline
  44. )
  45. const (
  46. NW Direction = iota
  47. N
  48. NE
  49. E
  50. SE
  51. S
  52. SW
  53. W
  54. Center
  55. )
  56. const (
  57. Horizontal Axis = iota
  58. Vertical
  59. )
  60. // Exact returns the Constraints with the minimum and maximum size
  61. // set to size.
  62. func Exact(size image.Point) Constraints {
  63. return Constraints{
  64. Min: size, Max: size,
  65. }
  66. }
  67. // FPt converts an point to a f32.Point.
  68. func FPt(p image.Point) f32.Point {
  69. return f32.Point{
  70. X: float32(p.X), Y: float32(p.Y),
  71. }
  72. }
  73. // Constrain a size so each dimension is in the range [min;max].
  74. func (c Constraints) Constrain(size image.Point) image.Point {
  75. if min := c.Min.X; size.X < min {
  76. size.X = min
  77. }
  78. if min := c.Min.Y; size.Y < min {
  79. size.Y = min
  80. }
  81. if max := c.Max.X; size.X > max {
  82. size.X = max
  83. }
  84. if max := c.Max.Y; size.Y > max {
  85. size.Y = max
  86. }
  87. return size
  88. }
  89. // AddMin returns a copy of Constraints with the Min constraint enlarged by up to delta
  90. // while still fitting within the Max constraint. The Max is unchanged, and the Min constraint
  91. // will not go negative.
  92. func (c Constraints) AddMin(delta image.Point) Constraints {
  93. c.Min = c.Min.Add(delta)
  94. if c.Min.X < 0 {
  95. c.Min.X = 0
  96. }
  97. if c.Min.Y < 0 {
  98. c.Min.Y = 0
  99. }
  100. c.Min = c.Constrain(c.Min)
  101. return c
  102. }
  103. // SubMax returns a copy of Constraints with the Max constraint shrunk by up to delta
  104. // while not going negative. The values of delta are expected to be positive.
  105. // The Min constraint is adjusted to fit within the new Max constraint.
  106. func (c Constraints) SubMax(delta image.Point) Constraints {
  107. c.Max = c.Max.Sub(delta)
  108. if c.Max.X < 0 {
  109. c.Max.X = 0
  110. }
  111. if c.Max.Y < 0 {
  112. c.Max.Y = 0
  113. }
  114. c.Min = c.Constrain(c.Min)
  115. return c
  116. }
  117. // Inset adds space around a widget by decreasing its maximum
  118. // constraints. The minimum constraints will be adjusted to ensure
  119. // they do not exceed the maximum.
  120. type Inset struct {
  121. Top, Bottom, Left, Right unit.Dp
  122. }
  123. // Layout a widget.
  124. func (in Inset) Layout(gtx Context, w Widget) Dimensions {
  125. top := gtx.Dp(in.Top)
  126. right := gtx.Dp(in.Right)
  127. bottom := gtx.Dp(in.Bottom)
  128. left := gtx.Dp(in.Left)
  129. mcs := gtx.Constraints
  130. mcs.Max.X -= left + right
  131. if mcs.Max.X < 0 {
  132. left = 0
  133. right = 0
  134. mcs.Max.X = 0
  135. }
  136. if mcs.Min.X > mcs.Max.X {
  137. mcs.Min.X = mcs.Max.X
  138. }
  139. mcs.Max.Y -= top + bottom
  140. if mcs.Max.Y < 0 {
  141. bottom = 0
  142. top = 0
  143. mcs.Max.Y = 0
  144. }
  145. if mcs.Min.Y > mcs.Max.Y {
  146. mcs.Min.Y = mcs.Max.Y
  147. }
  148. gtx.Constraints = mcs
  149. trans := op.Offset(image.Pt(left, top)).Push(gtx.Ops)
  150. dims := w(gtx)
  151. trans.Pop()
  152. return Dimensions{
  153. Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
  154. Baseline: dims.Baseline + bottom,
  155. }
  156. }
  157. // UniformInset returns an Inset with a single inset applied to all
  158. // edges.
  159. func UniformInset(v unit.Dp) Inset {
  160. return Inset{Top: v, Right: v, Bottom: v, Left: v}
  161. }
  162. // Layout a widget according to the direction.
  163. // The widget is called with the context constraints minimum cleared.
  164. func (d Direction) Layout(gtx Context, w Widget) Dimensions {
  165. macro := op.Record(gtx.Ops)
  166. csn := gtx.Constraints.Min
  167. switch d {
  168. case N, S:
  169. gtx.Constraints.Min.Y = 0
  170. case E, W:
  171. gtx.Constraints.Min.X = 0
  172. default:
  173. gtx.Constraints.Min = image.Point{}
  174. }
  175. dims := w(gtx)
  176. call := macro.Stop()
  177. sz := dims.Size
  178. if sz.X < csn.X {
  179. sz.X = csn.X
  180. }
  181. if sz.Y < csn.Y {
  182. sz.Y = csn.Y
  183. }
  184. p := d.Position(dims.Size, sz)
  185. defer op.Offset(p).Push(gtx.Ops).Pop()
  186. call.Add(gtx.Ops)
  187. return Dimensions{
  188. Size: sz,
  189. Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
  190. }
  191. }
  192. // Position calculates widget position according to the direction.
  193. func (d Direction) Position(widget, bounds image.Point) image.Point {
  194. var p image.Point
  195. switch d {
  196. case N, S, Center:
  197. p.X = (bounds.X - widget.X) / 2
  198. case NE, SE, E:
  199. p.X = bounds.X - widget.X
  200. }
  201. switch d {
  202. case W, Center, E:
  203. p.Y = (bounds.Y - widget.Y) / 2
  204. case SW, S, SE:
  205. p.Y = bounds.Y - widget.Y
  206. }
  207. return p
  208. }
  209. // Spacer adds space between widgets.
  210. type Spacer struct {
  211. Width, Height unit.Dp
  212. }
  213. func (s Spacer) Layout(gtx Context) Dimensions {
  214. return Dimensions{
  215. Size: gtx.Constraints.Constrain(image.Point{
  216. X: gtx.Dp(s.Width),
  217. Y: gtx.Dp(s.Height),
  218. }),
  219. }
  220. }
  221. func (a Alignment) String() string {
  222. switch a {
  223. case Start:
  224. return "Start"
  225. case End:
  226. return "End"
  227. case Middle:
  228. return "Middle"
  229. case Baseline:
  230. return "Baseline"
  231. default:
  232. panic("unreachable")
  233. }
  234. }
  235. // Convert a point in (x, y) coordinates to (main, cross) coordinates,
  236. // or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged
  237. // for the horizontal axis, or (y, x) for the vertical axis.
  238. func (a Axis) Convert(pt image.Point) image.Point {
  239. if a == Horizontal {
  240. return pt
  241. }
  242. return image.Pt(pt.Y, pt.X)
  243. }
  244. // FConvert a point in (x, y) coordinates to (main, cross) coordinates,
  245. // or vice versa. Specifically, FConvert((x, y)) returns (x, y) unchanged
  246. // for the horizontal axis, or (y, x) for the vertical axis.
  247. func (a Axis) FConvert(pt f32.Point) f32.Point {
  248. if a == Horizontal {
  249. return pt
  250. }
  251. return f32.Pt(pt.Y, pt.X)
  252. }
  253. // mainConstraint returns the min and max main constraints for axis a.
  254. func (a Axis) mainConstraint(cs Constraints) (int, int) {
  255. if a == Horizontal {
  256. return cs.Min.X, cs.Max.X
  257. }
  258. return cs.Min.Y, cs.Max.Y
  259. }
  260. // crossConstraint returns the min and max cross constraints for axis a.
  261. func (a Axis) crossConstraint(cs Constraints) (int, int) {
  262. if a == Horizontal {
  263. return cs.Min.Y, cs.Max.Y
  264. }
  265. return cs.Min.X, cs.Max.X
  266. }
  267. // constraints returns the constraints for axis a.
  268. func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
  269. if a == Horizontal {
  270. return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)}
  271. }
  272. return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)}
  273. }
  274. func (a Axis) String() string {
  275. switch a {
  276. case Horizontal:
  277. return "Horizontal"
  278. case Vertical:
  279. return "Vertical"
  280. default:
  281. panic("unreachable")
  282. }
  283. }
  284. func (d Direction) String() string {
  285. switch d {
  286. case NW:
  287. return "NW"
  288. case N:
  289. return "N"
  290. case NE:
  291. return "NE"
  292. case E:
  293. return "E"
  294. case SE:
  295. return "SE"
  296. case S:
  297. return "S"
  298. case SW:
  299. return "SW"
  300. case W:
  301. return "W"
  302. case Center:
  303. return "Center"
  304. default:
  305. panic("unreachable")
  306. }
  307. }