window.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. // Copyright 2016 The G3N Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. package gui
  5. import (
  6. "github.com/g3n/engine/gui/assets/icon"
  7. "github.com/g3n/engine/math32"
  8. "github.com/g3n/engine/window"
  9. )
  10. /*********************************************
  11. Window panel
  12. +-------------------------------------+---+
  13. | Title panel | X |
  14. +-------------------------------------+---+
  15. | Client (content) panel |
  16. | +-----------------------------------+ |
  17. | | | |
  18. | | | |
  19. | | | |
  20. | | | |
  21. | | | |
  22. | | | |
  23. | +-----------------------------------+ |
  24. | |
  25. +-----------------------------------------+
  26. *********************************************/
  27. // Window represents a window GUI element
  28. type Window struct {
  29. Panel // Embedded Panel
  30. styles *WindowStyles
  31. title *WindowTitle // internal optional title panel
  32. client Panel // internal client (content) panel
  33. resizable bool // Specifies whether the window is resizable
  34. drag bool // Whether the mouse buttons is pressed (i.e. when dragging)
  35. dragPadding float32 // Extra width used to resize (in addition to border sizes)
  36. // To keep track of which window borders the cursor is over
  37. overTop bool
  38. overRight bool
  39. overBottom bool
  40. overLeft bool
  41. // Minimum and maximum sizes
  42. minSize math32.Vector2
  43. maxSize math32.Vector2
  44. }
  45. // WindowStyle contains the styling of a Window
  46. type WindowStyle struct {
  47. PanelStyle
  48. TitleStyle WindowTitleStyle
  49. }
  50. // WindowStyles contains a WindowStyle for each valid GUI state
  51. type WindowStyles struct {
  52. Normal WindowStyle
  53. Over WindowStyle
  54. Focus WindowStyle
  55. Disabled WindowStyle
  56. }
  57. // NewWindow creates and returns a pointer to a new window with the
  58. // specified dimensions
  59. func NewWindow(width, height float32) *Window {
  60. w := new(Window)
  61. w.styles = &StyleDefault().Window
  62. w.Panel.Initialize(w, width, height)
  63. w.Panel.Subscribe(OnMouseDown, w.onMouse)
  64. w.Panel.Subscribe(OnMouseUp, w.onMouse)
  65. w.Panel.Subscribe(OnCursor, w.onCursor)
  66. w.Panel.Subscribe(OnCursorEnter, w.onCursor)
  67. w.Panel.Subscribe(OnCursorLeave, w.onCursor)
  68. w.Panel.Subscribe(OnResize, func(evname string, ev interface{}) { w.recalc() })
  69. w.client.Initialize(&w.client, 0, 0)
  70. w.Panel.Add(&w.client)
  71. w.dragPadding = 5
  72. w.recalc()
  73. w.update()
  74. return w
  75. }
  76. // SetResizable sets whether the window is resizable.
  77. func (w *Window) SetResizable(state bool) {
  78. w.resizable = state
  79. }
  80. // SetCloseButton sets whether the window has a close button on the top right.
  81. func (w *Window) SetCloseButton(state bool) {
  82. w.title.setCloseButton(state)
  83. }
  84. // SetTitle sets the title of the window.
  85. func (w *Window) SetTitle(text string) {
  86. if w.title == nil {
  87. w.title = newWindowTitle(w, text)
  88. w.title.Subscribe(OnCursor, w.onCursor)
  89. w.Panel.Add(w.title)
  90. } else {
  91. w.title.label.SetText(text)
  92. }
  93. w.update()
  94. w.recalc()
  95. }
  96. // Add adds a child panel to the client area of this window
  97. func (w *Window) Add(ichild IPanel) *Window {
  98. w.client.Add(ichild)
  99. return w
  100. }
  101. // Removes a child from the client (content) panel
  102. func (w *Window) Remove(ichild IPanel) bool {
  103. return w.client.Remove(ichild)
  104. }
  105. // SetLayout sets the layout of the client panel.
  106. func (w *Window) SetLayout(layout ILayout) {
  107. w.client.SetLayout(layout)
  108. }
  109. // onMouse process subscribed mouse events over the window
  110. func (w *Window) onMouse(evname string, ev interface{}) {
  111. switch evname {
  112. case OnMouseDown:
  113. // Move the window above everything contained in its parent
  114. parent := w.Parent().(IPanel).GetPanel()
  115. parent.SetTopChild(w)
  116. // If the click happened inside the draggable area, then set drag to true
  117. if w.overTop || w.overRight || w.overBottom || w.overLeft {
  118. w.drag = true
  119. Manager().SetCursorFocus(w)
  120. }
  121. case OnMouseUp:
  122. w.drag = false
  123. Manager().SetCursorFocus(nil)
  124. default:
  125. return
  126. }
  127. }
  128. // onCursor process subscribed cursor events over the window
  129. func (w *Window) onCursor(evname string, ev interface{}) {
  130. // If the window is not resizable we are not interested in cursor movements
  131. if !w.resizable {
  132. return
  133. }
  134. if evname == OnCursor {
  135. cev := ev.(*window.CursorEvent)
  136. // If already dragging - update window size and position depending
  137. // on the cursor position and the borders being dragged
  138. if w.drag {
  139. titleHeight := float32(0)
  140. titleLabelWidth := float32(0)
  141. titleCloseBtnWidth := float32(0)
  142. if w.title != nil {
  143. titleHeight = w.title.height
  144. titleLabelWidth = w.title.label.Width()
  145. if w.title.closeButton != nil {
  146. titleCloseBtnWidth = w.title.closeButton.Width()
  147. }
  148. }
  149. if w.overTop {
  150. delta := cev.Ypos - w.pospix.Y
  151. newHeight := w.Height() - delta
  152. minHeight := titleHeight
  153. if newHeight >= minHeight {
  154. w.SetPositionY(w.Position().Y + delta)
  155. w.SetHeight(math32.Max(newHeight, minHeight))
  156. } else {
  157. w.SetPositionY(w.Position().Y + w.Height() - minHeight)
  158. w.SetHeight(titleHeight)
  159. }
  160. }
  161. if w.overRight {
  162. delta := cev.Xpos - (w.pospix.X + w.width)
  163. newWidth := w.Width() + delta
  164. w.SetWidth(math32.Max(newWidth, titleLabelWidth+titleCloseBtnWidth))
  165. }
  166. if w.overBottom {
  167. delta := cev.Ypos - (w.pospix.Y + w.height)
  168. newHeight := w.Height() + delta
  169. w.SetHeight(math32.Max(newHeight, titleHeight))
  170. }
  171. if w.overLeft {
  172. delta := cev.Xpos - w.pospix.X
  173. newWidth := w.Width() - delta
  174. minWidth := titleLabelWidth + titleCloseBtnWidth
  175. if newWidth >= minWidth {
  176. w.SetPositionX(w.Position().X + delta)
  177. w.SetWidth(math32.Max(newWidth, minWidth))
  178. } else {
  179. w.SetPositionX(w.Position().X + w.Width() - minWidth)
  180. w.SetWidth(minWidth)
  181. }
  182. }
  183. } else {
  184. // Obtain cursor position relative to window
  185. cx := cev.Xpos - w.pospix.X
  186. cy := cev.Ypos - w.pospix.Y
  187. // Check if cursor is on the top of the window (border + drag margin)
  188. if cy <= w.borderSizes.Top {
  189. w.overTop = true
  190. } else {
  191. w.overTop = false
  192. }
  193. // Check if cursor is on the bottom of the window (border + drag margin)
  194. if cy >= w.height-w.borderSizes.Bottom-w.dragPadding {
  195. w.overBottom = true
  196. } else {
  197. w.overBottom = false
  198. }
  199. // Check if cursor is on the left of the window (border + drag margin)
  200. if cx <= w.borderSizes.Left+w.dragPadding {
  201. w.overLeft = true
  202. } else {
  203. w.overLeft = false
  204. }
  205. // Check if cursor is on the right of the window (border + drag margin)
  206. if cx >= w.width-w.borderSizes.Right-w.dragPadding {
  207. w.overRight = true
  208. } else {
  209. w.overRight = false
  210. }
  211. // Update cursor image based on cursor position
  212. if (w.overTop || w.overBottom) && !w.overRight && !w.overLeft {
  213. window.Get().SetCursor(window.VResizeCursor)
  214. } else if (w.overRight || w.overLeft) && !w.overTop && !w.overBottom {
  215. window.Get().SetCursor(window.HResizeCursor)
  216. } else if (w.overRight && w.overTop) || (w.overBottom && w.overLeft) {
  217. window.Get().SetCursor(window.DiagResize1Cursor)
  218. } else if (w.overRight && w.overBottom) || (w.overTop && w.overLeft) {
  219. window.Get().SetCursor(window.DiagResize2Cursor)
  220. }
  221. // If cursor is not near the border of the window then reset the cursor
  222. if !w.overTop && !w.overRight && !w.overBottom && !w.overLeft {
  223. window.Get().SetCursor(window.ArrowCursor)
  224. }
  225. }
  226. } else if evname == OnCursorLeave {
  227. window.Get().SetCursor(window.ArrowCursor)
  228. w.drag = false
  229. }
  230. }
  231. // update updates the window's visual state.
  232. func (w *Window) update() {
  233. if !w.Enabled() {
  234. w.applyStyle(&w.styles.Disabled)
  235. return
  236. }
  237. w.applyStyle(&w.styles.Normal)
  238. }
  239. // applyStyle applies a window style to the window.
  240. func (w *Window) applyStyle(s *WindowStyle) {
  241. w.SetBordersColor4(&s.BorderColor)
  242. w.SetBordersFrom(&s.Border)
  243. w.SetPaddingsFrom(&s.Padding)
  244. w.client.SetMarginsFrom(&s.Margin)
  245. w.client.SetColor4(&s.BgColor)
  246. if w.title != nil {
  247. w.title.applyStyle(&s.TitleStyle)
  248. }
  249. }
  250. // recalc recalculates the sizes and positions of the internal panels
  251. // from the outside to the inside.
  252. func (w *Window) recalc() {
  253. // Window title
  254. height := w.content.Height
  255. width := w.content.Width
  256. cx := float32(0)
  257. cy := float32(0)
  258. if w.title != nil {
  259. w.title.SetWidth(w.content.Width)
  260. w.title.recalc()
  261. height -= w.title.height
  262. cy = w.title.height
  263. }
  264. // Content area
  265. w.client.SetPosition(cx, cy)
  266. w.client.SetSize(width, height)
  267. }
  268. // WindowTitle represents the title bar of a Window
  269. type WindowTitle struct {
  270. Panel // Embedded panel
  271. win *Window // Window to which this title belongs
  272. label Label // Label for the title
  273. pressed bool // Whether the left mouse button is pressed
  274. closeButton *Button // The close button on the top right corner
  275. closeButtonVisible bool // Whether the close button is present
  276. // Last mouse coordinates
  277. mouseX float32
  278. mouseY float32
  279. }
  280. // WindowTitleStyle contains the styling for a window title.
  281. type WindowTitleStyle struct {
  282. PanelStyle
  283. FgColor math32.Color4
  284. }
  285. // newWindowTitle creates and returns a pointer to a window title panel.
  286. func newWindowTitle(win *Window, text string) *WindowTitle {
  287. wt := new(WindowTitle)
  288. wt.win = win
  289. wt.Panel.Initialize(wt, 0, 0)
  290. wt.label.initialize(text, StyleDefault().Font)
  291. wt.Panel.Add(&wt.label)
  292. wt.closeButton = NewButton("")
  293. wt.closeButton.SetIcon(icon.Close)
  294. wt.closeButton.Subscribe(OnCursorEnter, func(s string, i interface{}) {
  295. window.Get().SetCursor(window.ArrowCursor)
  296. })
  297. wt.closeButton.Subscribe(OnClick, func(s string, i interface{}) {
  298. wt.win.Parent().GetNode().Remove(wt.win)
  299. wt.win.Dispose()
  300. wt.win.Dispatch("gui.OnWindowClose", nil)
  301. })
  302. wt.Panel.Add(wt.closeButton)
  303. wt.closeButtonVisible = true
  304. wt.Subscribe(OnMouseDown, wt.onMouse)
  305. wt.Subscribe(OnMouseUp, wt.onMouse)
  306. wt.Subscribe(OnCursor, wt.onCursor)
  307. wt.Subscribe(OnCursorEnter, wt.onCursor)
  308. wt.Subscribe(OnCursorLeave, wt.onCursor)
  309. wt.recalc()
  310. return wt
  311. }
  312. // setCloseButton sets whether the close button is present on the top right corner.
  313. func (wt *WindowTitle) setCloseButton(state bool) {
  314. if state {
  315. wt.closeButtonVisible = true
  316. wt.Panel.Add(wt.closeButton)
  317. } else {
  318. wt.closeButtonVisible = false
  319. wt.Panel.Remove(wt.closeButton)
  320. }
  321. }
  322. // onMouse process subscribed mouse button events over the window title.
  323. func (wt *WindowTitle) onMouse(evname string, ev interface{}) {
  324. mev := ev.(*window.MouseEvent)
  325. switch evname {
  326. case OnMouseDown:
  327. wt.pressed = true
  328. wt.mouseX = mev.Xpos
  329. wt.mouseY = mev.Ypos
  330. Manager().SetCursorFocus(wt)
  331. case OnMouseUp:
  332. wt.pressed = false
  333. Manager().SetCursorFocus(nil)
  334. default:
  335. return
  336. }
  337. }
  338. // onCursor process subscribed cursor events over the window title.
  339. func (wt *WindowTitle) onCursor(evname string, ev interface{}) {
  340. if evname == OnCursorLeave {
  341. window.Get().SetCursor(window.ArrowCursor)
  342. wt.pressed = false
  343. } else if evname == OnCursor {
  344. if !wt.pressed {
  345. return
  346. }
  347. cev := ev.(*window.CursorEvent)
  348. dy := wt.mouseY - cev.Ypos
  349. dx := wt.mouseX - cev.Xpos
  350. wt.mouseX = cev.Xpos
  351. wt.mouseY = cev.Ypos
  352. posX := wt.win.Position().X - dx
  353. posY := wt.win.Position().Y - dy
  354. wt.win.SetPosition(posX, posY)
  355. }
  356. }
  357. // applyStyle applies the specified WindowTitleStyle.
  358. func (wt *WindowTitle) applyStyle(s *WindowTitleStyle) {
  359. wt.Panel.ApplyStyle(&s.PanelStyle)
  360. wt.label.SetColor4(&s.FgColor)
  361. }
  362. // recalc recalculates the height and position of the label in the title bar.
  363. func (wt *WindowTitle) recalc() {
  364. xpos := (wt.width - wt.label.width) / 2
  365. wt.label.SetPositionX(xpos)
  366. wt.SetContentHeight(wt.closeButton.Height())
  367. if wt.closeButtonVisible {
  368. wt.closeButton.SetPositionX(wt.width - wt.closeButton.width)
  369. }
  370. }