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.
 
 
 
 
 

330 lines
7.6 KiB

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