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.
 
 
 
 
 

315 lines
7.3 KiB

  1. package ui
  2. import (
  3. "fmt"
  4. "strings"
  5. "git.kirsle.net/go/render"
  6. )
  7. func init() {
  8. precomputeArrows()
  9. }
  10. // Tooltip attaches a mouse-over popup to another widget.
  11. type Tooltip struct {
  12. BaseWidget
  13. // Configurable attributes.
  14. Text string // Text to show in the tooltip.
  15. TextVariable *string // String pointer instead of text.
  16. Edge Edge // side to display tooltip on
  17. target Widget
  18. lineHeight int
  19. font render.Text
  20. }
  21. // Constants for tooltips.
  22. const (
  23. tooltipArrowSize = 5
  24. )
  25. // NewTooltip creates a new tooltip attached to a widget.
  26. func NewTooltip(target Widget, tt Tooltip) *Tooltip {
  27. w := &Tooltip{
  28. Text: tt.Text,
  29. TextVariable: tt.TextVariable,
  30. Edge: tt.Edge,
  31. target: target,
  32. }
  33. // Default style.
  34. w.Hide()
  35. w.SetBackground(render.RGBA(0, 0, 0, 230))
  36. w.font = render.Text{
  37. Size: 10,
  38. Color: render.White,
  39. Padding: 4,
  40. }
  41. // Add event bindings to the target widget.
  42. // - Show the tooltip on MouseOver
  43. // - Hide it on MouseOut
  44. // - Compute the tooltip when the parent widget Computes
  45. // - Present the tooltip when the parent widget Presents
  46. target.Handle(MouseOver, func(ed EventData) error {
  47. w.Show()
  48. return nil
  49. })
  50. target.Handle(MouseOut, func(ed EventData) error {
  51. w.Hide()
  52. return nil
  53. })
  54. target.Handle(Compute, func(ed EventData) error {
  55. w.Compute(ed.Engine)
  56. return nil
  57. })
  58. target.Handle(Present, func(ed EventData) error {
  59. w.Present(ed.Engine, w.Point())
  60. return nil
  61. })
  62. w.IDFunc(func() string {
  63. return fmt.Sprintf(`Tooltip<"%s">`, w.Value())
  64. })
  65. return w
  66. }
  67. // Value returns the current text displayed in the tooltop, whether from the
  68. // configured Text or the TextVariable pointer.
  69. func (w *Tooltip) Value() string {
  70. return w.text().Text
  71. }
  72. // text returns the raw render.Text holding the current value to be displayed
  73. // in the tooltip, either from Text or TextVariable.
  74. func (w *Tooltip) text() render.Text {
  75. if w.TextVariable != nil {
  76. w.font.Text = *w.TextVariable
  77. } else {
  78. w.font.Text = w.Text
  79. }
  80. return w.font
  81. }
  82. // Compute the size of the tooltip.
  83. func (w *Tooltip) Compute(e render.Engine) {
  84. // Compute the size based on the text.
  85. w.computeText(e)
  86. // Compute the position based on the Edge and the target widget.
  87. var (
  88. size = w.Size()
  89. target = w.target
  90. tSize = target.Size()
  91. tPoint = AbsolutePosition(target)
  92. moveTo render.Point
  93. )
  94. switch w.Edge {
  95. case Top:
  96. moveTo.Y = tPoint.Y - size.H - tooltipArrowSize
  97. moveTo.X = tPoint.X + (tSize.W / 2) - (size.W / 2)
  98. case Left:
  99. moveTo.X = tPoint.X - size.W - tooltipArrowSize
  100. moveTo.Y = tPoint.Y + (tSize.H / 2) - (size.H / 2)
  101. case Right:
  102. moveTo.X = tPoint.X + tSize.W + tooltipArrowSize
  103. moveTo.Y = tPoint.Y + (tSize.H / 2) - (size.H / 2)
  104. case Bottom:
  105. moveTo.Y = tPoint.Y + tSize.H + tooltipArrowSize
  106. moveTo.X = tPoint.X + (tSize.W / 2) - (size.W / 2)
  107. }
  108. // Adjust to keep the tooltip from clipping outside the window boundaries.
  109. {
  110. width, height := e.WindowSize()
  111. if moveTo.X < 0 {
  112. moveTo.X = 0
  113. } else if moveTo.X+size.W > width {
  114. moveTo.X = width - size.W
  115. }
  116. if moveTo.Y < 0 {
  117. moveTo.Y = 0
  118. } else if moveTo.Y+size.H > height {
  119. moveTo.Y = height - size.H
  120. }
  121. }
  122. w.MoveTo(moveTo)
  123. }
  124. // computeText handles the text compute, very similar to Label.Compute.
  125. func (w *Tooltip) computeText(e render.Engine) {
  126. text := w.text()
  127. lines := strings.Split(text.Text, "\n")
  128. // Max rect to encompass all lines of text.
  129. var maxRect = render.Rect{}
  130. for _, line := range lines {
  131. if line == "" {
  132. line = "<empty>"
  133. }
  134. text.Text = line // only this line at this time.
  135. rect, err := e.ComputeTextRect(text)
  136. if err != nil {
  137. panic(fmt.Sprintf("%s: failed to compute text rect: %s", w, err)) // TODO return an error
  138. }
  139. if rect.W > maxRect.W {
  140. maxRect.W = rect.W
  141. }
  142. maxRect.H += rect.H
  143. w.lineHeight = int(rect.H)
  144. }
  145. var (
  146. padX = w.font.Padding + w.font.PadX
  147. padY = w.font.Padding + w.font.PadY
  148. )
  149. w.Resize(render.Rect{
  150. W: maxRect.W + (padX * 2),
  151. H: maxRect.H + (padY * 2),
  152. })
  153. }
  154. // Present the tooltip.
  155. func (w *Tooltip) Present(e render.Engine, P render.Point) {
  156. if w.Hidden() {
  157. return
  158. }
  159. // Draw the text.
  160. w.presentText(e, P)
  161. // Draw the arrow.
  162. w.presentArrow(e, P)
  163. }
  164. // presentText draws the text similar to Label.
  165. func (w *Tooltip) presentText(e render.Engine, P render.Point) {
  166. var (
  167. text = w.text()
  168. padX = w.font.Padding + w.font.PadX
  169. padY = w.font.Padding + w.font.PadY
  170. )
  171. w.DrawBox(e, P)
  172. for i, line := range strings.Split(text.Text, "\n") {
  173. text.Text = line
  174. e.DrawText(text, render.Point{
  175. X: P.X + padX,
  176. Y: P.Y + padY + (i * w.lineHeight),
  177. })
  178. }
  179. }
  180. // presentArrow draws the arrow between the tooltip and its target widget.
  181. func (w *Tooltip) presentArrow(e render.Engine, P render.Point) {
  182. var (
  183. // size = w.Size()
  184. target = w.target
  185. tSize = target.Size()
  186. tPoint = AbsolutePosition(target)
  187. drawAt render.Point
  188. arrow [][]render.Point
  189. )
  190. switch w.Edge {
  191. case Top:
  192. arrow = arrowDown
  193. drawAt = render.Point{
  194. X: tPoint.X + (tSize.W / 2) - tooltipArrowSize,
  195. Y: tPoint.Y - tooltipArrowSize,
  196. }
  197. case Bottom:
  198. arrow = arrowUp
  199. drawAt = render.Point{
  200. X: tPoint.X + (tSize.W / 2) - tooltipArrowSize,
  201. Y: tPoint.Y + tSize.H,
  202. }
  203. case Left:
  204. arrow = arrowRight
  205. drawAt = render.Point{
  206. X: tPoint.X - tooltipArrowSize,
  207. Y: tPoint.Y + (tSize.H / 2) - tooltipArrowSize,
  208. }
  209. case Right:
  210. arrow = arrowLeft
  211. drawAt = render.Point{
  212. X: tPoint.X + tSize.W,
  213. Y: tPoint.Y + (tSize.H / 2) - tooltipArrowSize,
  214. }
  215. }
  216. drawArrow(e, w.Background(), drawAt, arrow)
  217. }
  218. // Draw an arrow at a given top/left coordinate.
  219. func drawArrow(e render.Engine, color render.Color, p render.Point, arrow [][]render.Point) {
  220. for _, row := range arrow {
  221. if len(row) == 1 {
  222. point := render.NewPoint(row[0].X, row[0].Y)
  223. point.Add(p)
  224. e.DrawPoint(color, point)
  225. } else {
  226. start := render.NewPoint(row[0].X, row[0].Y)
  227. end := render.NewPoint(row[1].X, row[1].Y)
  228. start.Add(p)
  229. end.Add(p)
  230. e.DrawLine(color, start, end)
  231. }
  232. }
  233. }
  234. // Arrows for the tooltip widget.
  235. var (
  236. arrowDown [][]render.Point
  237. arrowUp [][]render.Point
  238. arrowLeft [][]render.Point
  239. arrowRight [][]render.Point
  240. )
  241. func precomputeArrows() {
  242. arrowDown = [][]render.Point{
  243. {render.NewPoint(0, 0), render.NewPoint(10, 0)},
  244. {render.NewPoint(1, 1), render.NewPoint(9, 1)},
  245. {render.NewPoint(2, 2), render.NewPoint(8, 2)},
  246. {render.NewPoint(3, 3), render.NewPoint(7, 3)},
  247. {render.NewPoint(4, 4), render.NewPoint(6, 4)},
  248. {render.NewPoint(5, 5)},
  249. }
  250. arrowUp = [][]render.Point{
  251. {render.NewPoint(5, 0)},
  252. {render.NewPoint(4, 1), render.NewPoint(6, 1)},
  253. {render.NewPoint(3, 2), render.NewPoint(7, 2)},
  254. {render.NewPoint(2, 3), render.NewPoint(8, 3)},
  255. {render.NewPoint(1, 4), render.NewPoint(9, 4)},
  256. // {render.NewPoint(0, 5), render.NewPoint(10, 5)},
  257. }
  258. arrowLeft = [][]render.Point{
  259. {render.NewPoint(0, 5)},
  260. {render.NewPoint(1, 4), render.NewPoint(1, 6)},
  261. {render.NewPoint(2, 3), render.NewPoint(2, 7)},
  262. {render.NewPoint(3, 2), render.NewPoint(3, 8)},
  263. {render.NewPoint(4, 1), render.NewPoint(4, 9)},
  264. // {render.NewPoint(5, 0), render.NewPoint(5, 10)},
  265. }
  266. arrowRight = [][]render.Point{
  267. {render.NewPoint(0, 0), render.NewPoint(0, 10)},
  268. {render.NewPoint(1, 1), render.NewPoint(1, 9)},
  269. {render.NewPoint(2, 2), render.NewPoint(2, 8)},
  270. {render.NewPoint(3, 3), render.NewPoint(3, 7)},
  271. {render.NewPoint(4, 4), render.NewPoint(4, 6)},
  272. {render.NewPoint(5, 5)},
  273. }
  274. }