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.
 
 
 
 
 

289 lines
6.3 KiB

  1. package ui
  2. import (
  3. "fmt"
  4. "git.kirsle.net/go/render"
  5. "git.kirsle.net/go/ui/theme"
  6. )
  7. // MenuWidth sets the width of all popup menus. TODO, widths should be automatic.
  8. var MenuWidth = 180
  9. // Menu is a frame that holds menu items. It is the
  10. type Menu struct {
  11. BaseWidget
  12. Name string
  13. supervisor *Supervisor
  14. body *Frame
  15. items []*MenuItem
  16. }
  17. // NewMenu creates a new Menu. It is hidden by default. Usually you'll
  18. // use it with a MenuButton or in a right-click handler.
  19. func NewMenu(name string) *Menu {
  20. w := &Menu{
  21. Name: name,
  22. body: NewFrame(name + ":Body"),
  23. items: []*MenuItem{},
  24. }
  25. w.body.Configure(Config{
  26. Width: MenuWidth,
  27. Height: 100,
  28. BorderSize: 0,
  29. BorderStyle: BorderRaised,
  30. Background: theme.ButtonBackgroundColor,
  31. })
  32. w.body.SetParent(w)
  33. w.IDFunc(func() string {
  34. return fmt.Sprintf("Menu<%s>", w.Name)
  35. })
  36. return w
  37. }
  38. // Children returns the child frame of the menu.
  39. func (w *Menu) Children() []Widget {
  40. return []Widget{
  41. w.body,
  42. }
  43. }
  44. // Supervise the Menu. This will add all current and future MenuItem widgets
  45. // to the supervisor.
  46. func (w *Menu) Supervise(s *Supervisor) {
  47. w.supervisor = s
  48. for _, item := range w.items {
  49. w.supervisor.Add(item)
  50. }
  51. }
  52. // Compute the menu
  53. func (w *Menu) Compute(e render.Engine) {
  54. w.body.Compute(e)
  55. // TODO: ideally the Frame Pack Compute would fix the size of the body
  56. // for the height to match the height of the menu items... but for now
  57. // manually set the height.
  58. var maxWidth int
  59. var height int
  60. for _, child := range w.body.Children() {
  61. size := child.Size()
  62. if size.W > maxWidth {
  63. maxWidth = size.W
  64. }
  65. height += child.Size().H
  66. }
  67. w.body.Resize(render.NewRect(maxWidth, height))
  68. // Call the BaseWidget Compute in case we have subscribers.
  69. w.BaseWidget.Compute(e)
  70. }
  71. // Present the menu
  72. func (w *Menu) Present(e render.Engine, p render.Point) {
  73. w.body.Present(e, p)
  74. // Call the BaseWidget Present in case we have subscribers.
  75. w.BaseWidget.Present(e, p)
  76. }
  77. // AddItem quickly adds an item to a menu.
  78. func (w *Menu) AddItem(label string, command func()) *MenuItem {
  79. menu := NewMenuItem(label, "", command)
  80. // Add a Click handler that closes the menu when a selection is made.
  81. menu.Handle(Click, w.menuClickHandler)
  82. w.Pack(menu)
  83. return menu
  84. }
  85. // AddItemAccel quickly adds an item to a menu with a shortcut key label.
  86. func (w *Menu) AddItemAccel(label string, accelerator string, command func()) *MenuItem {
  87. menu := NewMenuItem(label, accelerator, command)
  88. // Add a Click handler that closes the menu when a selection is made.
  89. menu.Handle(Click, w.menuClickHandler)
  90. w.Pack(menu)
  91. return menu
  92. }
  93. // Click handler for all menu items, to also close the menu behind them.
  94. func (w *Menu) menuClickHandler(ed EventData) error {
  95. if w.supervisor != nil {
  96. w.supervisor.PopModal(w)
  97. }
  98. return nil
  99. }
  100. // AddSeparator adds a separator bar to the menu to delineate items.
  101. func (w *Menu) AddSeparator() *MenuItem {
  102. sep := NewMenuSeparator()
  103. w.Pack(sep)
  104. return sep
  105. }
  106. // Pack a menu item onto the menu.
  107. func (w *Menu) Pack(item *MenuItem) {
  108. w.items = append(w.items, item)
  109. w.body.Pack(item, Pack{
  110. Side: N,
  111. FillX: true,
  112. })
  113. if w.supervisor != nil {
  114. w.supervisor.Add(item)
  115. }
  116. }
  117. // Size returns the size of the menu's body.
  118. func (w *Menu) Size() render.Rect {
  119. return w.body.Size()
  120. }
  121. // Rect returns the rect of the menu's body.
  122. func (w *Menu) Rect() render.Rect {
  123. // TODO: the height reports wrong (0), manually add up the MenuItem sizes.
  124. // This manifests in Supervisor.runWidgetEvents when checking if the cursor
  125. // clicked outside the rect of the active menu modal.
  126. rect := w.body.Rect()
  127. rect.H = 0
  128. for _, child := range w.body.Children() {
  129. rect.H += child.Size().H
  130. }
  131. return rect
  132. }
  133. // MenuItem is an item in a Menu.
  134. type MenuItem struct {
  135. Button
  136. Label string
  137. Accelerator string
  138. Command func()
  139. separator bool
  140. button *Button
  141. // store of most recent bg color set on a menu item
  142. cacheBg render.Color
  143. cacheFg render.Color
  144. }
  145. // NewMenuItem creates a new menu item.
  146. func NewMenuItem(label, accelerator string, command func()) *MenuItem {
  147. w := &MenuItem{
  148. Label: label,
  149. Accelerator: accelerator,
  150. Command: command,
  151. }
  152. w.IDFunc(func() string {
  153. return fmt.Sprintf("MenuItem<%s>", w.Label)
  154. })
  155. font := DefaultFont
  156. font.Color = render.Black
  157. font.PadX = 12
  158. font.PadY = 2
  159. // The button child will be a Frame so we can have a left-aligned label
  160. // and a right-aligned accelerator.
  161. frame := NewFrame(label + ":Frame")
  162. frame.Configure(Config{
  163. Width: MenuWidth,
  164. })
  165. {
  166. // Left of frame: menu item label
  167. lbl := NewLabel(Label{
  168. Text: label,
  169. Font: font,
  170. })
  171. frame.Pack(lbl, Pack{
  172. Side: W,
  173. })
  174. // On the right: accelerator shortcut key
  175. if accelerator != "" {
  176. accel := NewLabel(Label{
  177. Text: accelerator,
  178. Font: font,
  179. })
  180. frame.Pack(accel, Pack{
  181. Side: E,
  182. })
  183. }
  184. }
  185. w.Button.child = frame
  186. w.Button.Configure(Config{
  187. BorderSize: 0,
  188. Background: theme.ButtonBackgroundColor,
  189. })
  190. w.Button.Handle(MouseOver, func(ed EventData) error {
  191. w.setHoverStyle(true)
  192. return nil
  193. })
  194. w.Button.Handle(MouseOut, func(ed EventData) error {
  195. w.setHoverStyle(false)
  196. return nil
  197. })
  198. w.Button.Handle(Click, func(ed EventData) error {
  199. w.Command()
  200. return nil
  201. })
  202. // Assign the button
  203. return w
  204. }
  205. // NewMenuSeparator creates a separator menu item.
  206. func NewMenuSeparator() *MenuItem {
  207. w := &MenuItem{
  208. separator: true,
  209. }
  210. w.IDFunc(func() string {
  211. return "MenuItem<separator>"
  212. })
  213. w.Button.child = NewFrame("Menu Separator")
  214. w.Button.Configure(Config{
  215. Width: MenuWidth,
  216. Height: 2,
  217. BorderSize: 1,
  218. BorderStyle: BorderSunken,
  219. BorderColor: render.Grey,
  220. })
  221. return w
  222. }
  223. // Set the hover styling (text/bg color)
  224. func (w *MenuItem) setHoverStyle(hovering bool) {
  225. // Note: this only works if the MenuItem is using the standard
  226. // Frame and Labels layout created by AddItem(). If not, this function
  227. // does nothing.
  228. // BG color.
  229. if hovering {
  230. w.cacheBg = w.Background()
  231. w.SetBackground(render.SkyBlue)
  232. } else {
  233. w.SetBackground(w.cacheBg)
  234. }
  235. frame, ok := w.Button.child.(*Frame)
  236. if !ok {
  237. return
  238. }
  239. for _, widget := range frame.Children() {
  240. if label, ok := widget.(*Label); ok {
  241. if hovering {
  242. w.cacheFg = label.Font.Color
  243. label.Font.Color = render.White
  244. } else {
  245. label.Font.Color = w.cacheFg
  246. }
  247. }
  248. }
  249. }