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.
 
 
 
 
 

196 lines
4.9 KiB

  1. package ui
  2. import (
  3. "fmt"
  4. "git.kirsle.net/go/render"
  5. "git.kirsle.net/go/ui/theme"
  6. )
  7. // MenuButton is a button that opens a menu when clicked.
  8. //
  9. // After creating a MenuButton, call AddItem() to add options and callback
  10. // functions to fill out the menu. When the MenuButton is clicked, its menu
  11. // will be drawn and take modal priority in the Supervisor.
  12. type MenuButton struct {
  13. Button
  14. name string
  15. supervisor *Supervisor
  16. menu *Menu
  17. }
  18. // NewMenuButton creates a new MenuButton (labels recommended).
  19. //
  20. // If the child is a Label, this function will set some sensible padding on
  21. // its font if the Label does not already have non-zero padding set.
  22. func NewMenuButton(name string, child Widget) *MenuButton {
  23. w := &MenuButton{
  24. name: name,
  25. }
  26. w.Button.child = child
  27. // If it's a Label (most common), set sensible default padding.
  28. if label, ok := child.(*Label); ok {
  29. if label.Font.Padding == 0 && label.Font.PadX == 0 && label.Font.PadY == 0 {
  30. label.Font.PadX = 8
  31. label.Font.PadY = 4
  32. }
  33. }
  34. w.IDFunc(func() string {
  35. return fmt.Sprintf("MenuButton<%s>", name)
  36. })
  37. w.setup()
  38. return w
  39. }
  40. // Supervise the MenuButton. This is necessary for the pop-up menu to work
  41. // when the button is clicked.
  42. func (w *MenuButton) Supervise(s *Supervisor) {
  43. w.initMenu()
  44. w.supervisor = s
  45. w.menu.Supervise(s)
  46. }
  47. // AddItem adds a new option to the MenuButton's menu.
  48. func (w *MenuButton) AddItem(label string, f func()) {
  49. w.initMenu()
  50. w.menu.AddItem(label, f)
  51. }
  52. // AddItemAccel adds a new menu option with hotkey text.
  53. func (w *MenuButton) AddItemAccel(label string, accelerator string, f func()) *MenuItem {
  54. w.initMenu()
  55. return w.menu.AddItemAccel(label, accelerator, f)
  56. }
  57. // AddSeparator adds a separator to the menu.
  58. func (w *MenuButton) AddSeparator() {
  59. w.initMenu()
  60. w.menu.AddSeparator()
  61. }
  62. // Compute to re-evaluate the button state (in the case of radio buttons where
  63. // a different button will affect the state of this one when clicked).
  64. func (w *MenuButton) Compute(e render.Engine) {
  65. if w.menu != nil {
  66. w.menu.Compute(e)
  67. w.positionMenu(e)
  68. }
  69. }
  70. // positionMenu sets the position where the pop-up menu will appear when
  71. // the button is clicked. Usually, the menu appears below and to the right of
  72. // the button. But if the menu will hit a window boundary, its position will
  73. // be adjusted to fit the window while trying not to overlap its own button.
  74. func (w *MenuButton) positionMenu(e render.Engine) {
  75. var (
  76. // Position and size of the MenuButton button.
  77. buttonPoint = w.Point()
  78. buttonSize = w.Size()
  79. // Size of the actual desktop window.
  80. Width, Height = e.WindowSize()
  81. )
  82. // Ideal location: below and to the right of the button.
  83. w.menu.MoveTo(render.Point{
  84. X: buttonPoint.X,
  85. Y: buttonPoint.Y + buttonSize.H + w.BoxThickness(2),
  86. })
  87. var (
  88. // Size of the menu.
  89. menuPoint = w.menu.Point()
  90. menuSize = w.menu.Rect()
  91. margin = 8 // keep away from directly touching window edges
  92. topMargin = 32 // keep room for standard Menu Bar
  93. )
  94. // Will we clip out the bottom of the window?
  95. if menuPoint.Y+menuSize.H+margin > Height {
  96. // Put us above the button instead, with the bottom of the
  97. // menu touching the top of the button.
  98. menuPoint = render.Point{
  99. X: buttonPoint.X,
  100. Y: buttonPoint.Y - menuSize.H - w.BoxThickness(2),
  101. }
  102. // If this would put us over the TOP edge of the window now,
  103. // cap the movement so the top of the menu is visible. We can't
  104. // avoid overlapping the button with the menu so might as well
  105. // start now.
  106. if menuPoint.Y < topMargin {
  107. menuPoint.Y = topMargin
  108. }
  109. w.menu.MoveTo(menuPoint)
  110. }
  111. // Will we clip out the right of the window?
  112. if menuPoint.X+menuSize.W > Width {
  113. // Move us in from the right side of the window.
  114. var delta = Width - menuSize.W - margin
  115. w.menu.MoveTo(render.Point{
  116. X: delta,
  117. Y: menuPoint.Y,
  118. })
  119. }
  120. _ = Width
  121. }
  122. // setup the common things between checkboxes and radioboxes.
  123. func (w *MenuButton) setup() {
  124. w.Configure(Config{
  125. BorderSize: 1,
  126. BorderStyle: BorderSolid,
  127. Background: theme.ButtonBackgroundColor,
  128. })
  129. w.Handle(MouseOver, func(ed EventData) error {
  130. w.hovering = true
  131. w.SetBorderStyle(BorderRaised)
  132. return nil
  133. })
  134. w.Handle(MouseOut, func(ed EventData) error {
  135. w.hovering = false
  136. w.SetBorderStyle(BorderSolid)
  137. return nil
  138. })
  139. w.Handle(MouseDown, func(ed EventData) error {
  140. w.clicked = true
  141. w.SetBorderStyle(BorderSunken)
  142. return nil
  143. })
  144. w.Handle(MouseUp, func(ed EventData) error {
  145. w.clicked = false
  146. return nil
  147. })
  148. w.Handle(Click, func(ed EventData) error {
  149. // Are we properly configured?
  150. if w.supervisor != nil && w.menu != nil {
  151. w.menu.Show()
  152. w.supervisor.PushModal(w.menu)
  153. }
  154. return nil
  155. })
  156. }
  157. // initialize the Menu widget.
  158. func (w *MenuButton) initMenu() {
  159. if w.menu == nil {
  160. w.menu = NewMenu(w.name + ":Menu")
  161. w.menu.Hide()
  162. // Handle closing the menu when clicked outside.
  163. w.menu.Handle(CloseModal, func(ed EventData) error {
  164. ed.Supervisor.PopModal(w.menu)
  165. return nil
  166. })
  167. }
  168. }