tabbar.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  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. "fmt"
  7. "github.com/g3n/engine/window"
  8. )
  9. // TabBar is a panel which can contain other panels arranged in horizontal Tabs.
  10. // Only one panel is visible at a time.
  11. // To show another panel the corresponding Tab must be selected.
  12. type TabBar struct {
  13. Panel // Embedded panel
  14. styles *TabBarStyles // Pointer to current styles
  15. tabs []*Tab // Array of tabs
  16. separator Panel // Separator Panel
  17. listButton *Label // Icon for tab list button
  18. list *List // List for not visible tabs
  19. selected int // Index of the selected tab
  20. cursorOver bool // Cursor over TabBar panel flag
  21. }
  22. // TabBarStyle describes the style of the TabBar
  23. type TabBarStyle BasicStyle
  24. // TabBarStyles describes all the TabBarStyles
  25. type TabBarStyles struct {
  26. SepHeight float32 // Separator width
  27. ListButtonIcon string // Icon for list button
  28. ListButtonPaddings RectBounds // Paddings for list button
  29. Normal TabBarStyle // Style for normal exhibition
  30. Over TabBarStyle // Style when cursor is over the TabBar
  31. Focus TabBarStyle // Style when the TabBar has key focus
  32. Disabled TabBarStyle // Style when the TabBar is disabled
  33. Tab TabStyles // Style for Tabs
  34. }
  35. // TabStyle describes the style of the individual Tabs header
  36. type TabStyle BasicStyle
  37. // TabStyles describes all Tab styles
  38. type TabStyles struct {
  39. IconPaddings RectBounds // Paddings for optional icon
  40. ImagePaddings RectBounds // Paddings for optional image
  41. IconClose string // Codepoint for close icon in Tab header
  42. Normal TabStyle // Style for normal exhibition
  43. Over TabStyle // Style when cursor is over the Tab
  44. Focus TabStyle // Style when the Tab has key focus
  45. Disabled TabStyle // Style when the Tab is disabled
  46. Selected TabStyle // Style when the Tab is selected
  47. }
  48. // NewTabBar creates and returns a pointer to a new TabBar widget
  49. // with the specified width and height
  50. func NewTabBar(width, height float32) *TabBar {
  51. // Creates new TabBar
  52. tb := new(TabBar)
  53. tb.Initialize(tb, width, height)
  54. tb.styles = &StyleDefault().TabBar
  55. tb.tabs = make([]*Tab, 0)
  56. tb.selected = -1
  57. // Creates separator panel (between the tab headers and content panel)
  58. tb.separator.Initialize(&tb.separator, 0, 0)
  59. tb.Add(&tb.separator)
  60. // Create list for contained tabs not visible
  61. tb.list = NewVList(0, 0)
  62. tb.list.Subscribe(OnMouseDownOut, func(evname string, ev interface{}) {
  63. tb.list.SetVisible(false)
  64. })
  65. tb.list.Subscribe(OnChange, tb.onListChange)
  66. tb.Add(tb.list)
  67. // Creates list icon button
  68. tb.listButton = NewIcon(tb.styles.ListButtonIcon)
  69. tb.listButton.SetPaddingsFrom(&tb.styles.ListButtonPaddings)
  70. tb.listButton.Subscribe(OnMouseDown, tb.onListButton)
  71. tb.Add(tb.listButton)
  72. // Subscribe to panel events
  73. tb.Subscribe(OnCursorEnter, tb.onCursor)
  74. tb.Subscribe(OnCursorLeave, tb.onCursor)
  75. tb.Subscribe(OnEnable, func(name string, ev interface{}) { tb.update() })
  76. tb.Subscribe(OnResize, func(name string, ev interface{}) { tb.recalc() })
  77. tb.recalc()
  78. tb.update()
  79. return tb
  80. }
  81. // AddTab creates and adds a new Tab panel with the specified header text
  82. // at the end of this TabBar list of tabs.
  83. // Returns the pointer to thew new Tab.
  84. func (tb *TabBar) AddTab(text string) *Tab {
  85. tab := tb.InsertTab(text, len(tb.tabs))
  86. tb.SetSelected(len(tb.tabs) - 1)
  87. return tab
  88. }
  89. // InsertTab creates and inserts a new Tab panel with the specified header text
  90. // at the specified position in the TabBar from left to right.
  91. // Returns the pointer to the new Tab or nil if the position is invalid.
  92. func (tb *TabBar) InsertTab(text string, pos int) *Tab {
  93. // Checks position to insert into
  94. if pos < 0 || pos > len(tb.tabs) {
  95. return nil
  96. }
  97. // Inserts created Tab at the specified position
  98. tab := newTab(text, tb, &tb.styles.Tab)
  99. tb.tabs = append(tb.tabs, nil)
  100. copy(tb.tabs[pos+1:], tb.tabs[pos:])
  101. tb.tabs[pos] = tab
  102. tb.Add(&tab.header)
  103. tb.update()
  104. tb.recalc()
  105. return tab
  106. }
  107. // RemoveTab removes the tab at the specified position in the TabBar.
  108. // Returns an error if the position is invalid.
  109. func (tb *TabBar) RemoveTab(pos int) error {
  110. // Check position to remove from
  111. if pos < 0 || pos >= len(tb.tabs) {
  112. return fmt.Errorf("Invalid tab position:%d", pos)
  113. }
  114. // Remove tab from TabBar panel
  115. tab := tb.tabs[pos]
  116. tb.Remove(&tab.header)
  117. if tab.content != nil {
  118. tb.Remove(tab.content)
  119. }
  120. // Remove tab from tabbar array
  121. copy(tb.tabs[pos:], tb.tabs[pos+1:])
  122. tb.tabs[len(tb.tabs)-1] = nil
  123. tb.tabs = tb.tabs[:len(tb.tabs)-1]
  124. // If removed tab was selected, selects other tab.
  125. if tb.selected == pos {
  126. // Try to select tab at right
  127. if len(tb.tabs) > pos {
  128. tb.tabs[pos].setSelected(true)
  129. // Otherwise select tab at left
  130. } else if pos > 0 {
  131. tb.tabs[pos-1].setSelected(true)
  132. }
  133. }
  134. tb.update()
  135. tb.recalc()
  136. return nil
  137. }
  138. // MoveTab moves a Tab to another position in the Tabs list
  139. func (tb *TabBar) MoveTab(src, dest int) error {
  140. // Check source position
  141. if src < 0 || src >= len(tb.tabs) {
  142. return fmt.Errorf("Invalid tab source position:%d", src)
  143. }
  144. // Check destination position
  145. if dest < 0 || dest >= len(tb.tabs) {
  146. return fmt.Errorf("Invalid tab destination position:%d", dest)
  147. }
  148. if src == dest {
  149. return nil
  150. }
  151. tabDest := tb.tabs[dest]
  152. tb.tabs[dest] = tb.tabs[src]
  153. tb.tabs[src] = tabDest
  154. tb.recalc()
  155. return nil
  156. }
  157. // TabCount returns the current number of Tabs in the TabBar
  158. func (tb *TabBar) TabCount() int {
  159. return len(tb.tabs)
  160. }
  161. // TabAt returns the pointer of the Tab object at the specified position.
  162. // Return nil if the position is invalid
  163. func (tb *TabBar) TabAt(pos int) *Tab {
  164. if pos < 0 || pos >= len(tb.tabs) {
  165. return nil
  166. }
  167. return tb.tabs[pos]
  168. }
  169. // TabPosition returns the position of the Tab specified by its pointer
  170. func (tb *TabBar) TabPosition(tab *Tab) int {
  171. for i := 0; i < len(tb.tabs); i++ {
  172. if tb.tabs[i] == tab {
  173. return i
  174. }
  175. }
  176. return -1
  177. }
  178. // SetSelected sets the selected tab of the TabBar to the tab with the specified position.
  179. // Returns the pointer of the selected tab or nil if the position is invalid.
  180. func (tb *TabBar) SetSelected(pos int) *Tab {
  181. if pos < 0 || pos >= len(tb.tabs) {
  182. return nil
  183. }
  184. for i := 0; i < len(tb.tabs); i++ {
  185. if i == pos {
  186. tb.tabs[i].setSelected(true)
  187. } else {
  188. tb.tabs[i].setSelected(false)
  189. }
  190. }
  191. tb.selected = pos
  192. return tb.tabs[pos]
  193. }
  194. // Selected returns the position of the selected Tab.
  195. // Returns value < 0 if there is no selected Tab.
  196. func (tb *TabBar) Selected() int {
  197. return tb.selected
  198. }
  199. // onCursor process subscribed cursor events
  200. func (tb *TabBar) onCursor(evname string, ev interface{}) {
  201. switch evname {
  202. case OnCursorEnter:
  203. tb.cursorOver = true
  204. tb.update()
  205. case OnCursorLeave:
  206. tb.cursorOver = false
  207. tb.update()
  208. default:
  209. return
  210. }
  211. }
  212. // onListButtonMouse process subscribed MouseButton events over the list button
  213. func (tb *TabBar) onListButton(evname string, ev interface{}) {
  214. switch evname {
  215. case OnMouseDown:
  216. if !tb.list.Visible() {
  217. tb.list.SetVisible(true)
  218. }
  219. default:
  220. return
  221. }
  222. }
  223. // onListChange process OnChange event from the tab list
  224. func (tb *TabBar) onListChange(evname string, ev interface{}) {
  225. selected := tb.list.Selected()
  226. pos := selected[0].GetPanel().UserData().(int)
  227. log.Error("onListChange:%v", pos)
  228. tb.SetSelected(pos)
  229. tb.list.SetVisible(false)
  230. }
  231. // applyStyle applies the specified TabBar style
  232. func (tb *TabBar) applyStyle(s *TabBarStyle) {
  233. tb.Panel.ApplyStyle(&s.PanelStyle)
  234. tb.separator.SetColor4(&s.BorderColor)
  235. }
  236. // recalc recalculates and updates the positions of all tabs
  237. func (tb *TabBar) recalc() {
  238. // Determines how many tabs could be fully shown
  239. iconWidth := tb.listButton.Width()
  240. availWidth := tb.ContentWidth() - iconWidth
  241. var tabWidth float32
  242. var totalWidth float32
  243. var count int
  244. for i := 0; i < len(tb.tabs); i++ {
  245. tab := tb.tabs[i]
  246. minw := tab.minWidth()
  247. if minw > tabWidth {
  248. tabWidth = minw
  249. }
  250. totalWidth = float32(count+1) * tabWidth
  251. if totalWidth > availWidth {
  252. break
  253. }
  254. count++
  255. }
  256. // If there are more Tabs that can be shown, shows list button
  257. if count < len(tb.tabs) {
  258. // Sets the list button visible
  259. tb.listButton.SetVisible(true)
  260. height := tb.tabs[0].header.Height()
  261. iy := (height - tb.listButton.Height()) / 2
  262. tb.listButton.SetPosition(availWidth, iy)
  263. // Sets the tab list position and size
  264. listWidth := float32(200)
  265. lx := tb.ContentWidth() - listWidth
  266. ly := height + 1
  267. tb.list.SetPosition(lx, ly)
  268. tb.list.SetSize(listWidth, 200)
  269. tb.SetTopChild(tb.list)
  270. } else {
  271. tb.listButton.SetVisible(false)
  272. tb.list.SetVisible(false)
  273. }
  274. tb.list.Clear()
  275. var headerx float32
  276. // When there is available space limits the with of the tabs
  277. maxTabWidth := availWidth / float32(count)
  278. if tabWidth < maxTabWidth {
  279. tabWidth += (maxTabWidth - tabWidth) / 4
  280. }
  281. for i := 0; i < len(tb.tabs); i++ {
  282. tab := tb.tabs[i]
  283. // Recalculate Tab header and sets its position
  284. tab.recalc(tabWidth)
  285. tab.header.SetPosition(headerx, 0)
  286. // Sets size and position of the Tab content panel
  287. if tab.content != nil {
  288. cpan := tab.content.GetPanel()
  289. contenty := tab.header.Height() + tb.styles.SepHeight
  290. cpan.SetWidth(tb.ContentWidth())
  291. cpan.SetHeight(tb.ContentHeight() - contenty)
  292. cpan.SetPosition(0, contenty)
  293. }
  294. headerx += tab.header.Width()
  295. // If Tab can be shown set its header visible
  296. if i < count {
  297. tab.header.SetVisible(true)
  298. // Otherwise insert tab text in List
  299. } else {
  300. tab.header.SetVisible(false)
  301. item := NewImageLabel(tab.label.Text())
  302. item.SetUserData(i)
  303. tb.list.Add(item)
  304. }
  305. }
  306. // Sets the separator size, position and visibility
  307. if len(tb.tabs) > 0 {
  308. tb.separator.SetSize(tb.ContentWidth(), tb.styles.SepHeight)
  309. tb.separator.SetPositionY(tb.tabs[0].header.Height())
  310. tb.separator.SetVisible(true)
  311. } else {
  312. tb.separator.SetVisible(false)
  313. }
  314. }
  315. // update updates the TabBar visual state
  316. func (tb *TabBar) update() {
  317. if !tb.Enabled() {
  318. tb.applyStyle(&tb.styles.Disabled)
  319. return
  320. }
  321. if tb.cursorOver {
  322. tb.applyStyle(&tb.styles.Over)
  323. return
  324. }
  325. tb.applyStyle(&tb.styles.Normal)
  326. }
  327. //
  328. // Tab describes an individual tab of the TabBar
  329. //
  330. type Tab struct {
  331. tb *TabBar // Pointer to parent *TabBar
  332. styles *TabStyles // Pointer to Tab current styles
  333. header Panel // Tab header
  334. label *Label // Tab user label
  335. iconClose *Label // Tab close icon
  336. icon *Label // Tab optional user icon
  337. image *Image // Tab optional user image
  338. bottom Panel // Panel to cover the bottom edge of the Tab
  339. content IPanel // User content panel
  340. cursorOver bool
  341. selected bool
  342. pinned bool
  343. }
  344. // newTab creates and returns a pointer to a new Tab
  345. func newTab(text string, tb *TabBar, styles *TabStyles) *Tab {
  346. tab := new(Tab)
  347. tab.tb = tb
  348. tab.styles = styles
  349. // Setup the header panel
  350. tab.header.Initialize(&tab.header, 0, 0)
  351. tab.label = NewLabel(text)
  352. tab.iconClose = NewIcon(styles.IconClose)
  353. tab.header.Add(tab.label)
  354. tab.header.Add(tab.iconClose)
  355. // Creates the bottom panel
  356. tab.bottom.Initialize(&tab.bottom, 0, 0)
  357. tab.bottom.SetBounded(false)
  358. tab.bottom.SetColor4(&tab.styles.Selected.BgColor)
  359. tab.header.Add(&tab.bottom)
  360. // Subscribe to header panel events
  361. tab.header.Subscribe(OnCursorEnter, tab.onCursor)
  362. tab.header.Subscribe(OnCursorLeave, tab.onCursor)
  363. tab.header.Subscribe(OnMouseDown, tab.onMouseHeader)
  364. tab.iconClose.Subscribe(OnMouseDown, tab.onMouseIcon)
  365. tab.update()
  366. return tab
  367. }
  368. // onCursor process subscribed cursor events over the tab header
  369. func (tab *Tab) onCursor(evname string, ev interface{}) {
  370. switch evname {
  371. case OnCursorEnter:
  372. tab.cursorOver = true
  373. tab.update()
  374. case OnCursorLeave:
  375. tab.cursorOver = false
  376. tab.update()
  377. default:
  378. return
  379. }
  380. }
  381. // onMouse process subscribed mouse events over the tab header
  382. func (tab *Tab) onMouseHeader(evname string, ev interface{}) {
  383. if evname == OnMouseDown && ev.(*window.MouseEvent).Button == window.MouseButtonLeft {
  384. tab.tb.SetSelected(tab.tb.TabPosition(tab))
  385. }
  386. }
  387. // onMouseIcon process subscribed mouse events over the tab close icon
  388. func (tab *Tab) onMouseIcon(evname string, ev interface{}) {
  389. switch evname {
  390. case OnMouseDown:
  391. tab.tb.RemoveTab(tab.tb.TabPosition(tab))
  392. default:
  393. return
  394. }
  395. }
  396. // SetText sets the text of the tab header
  397. func (tab *Tab) SetText(text string) *Tab {
  398. tab.label.SetText(text)
  399. // Needs to recalculate all Tabs because this Tab width will change
  400. tab.tb.recalc()
  401. return tab
  402. }
  403. // SetIcon sets the optional icon of the Tab header
  404. func (tab *Tab) SetIcon(icon string) *Tab {
  405. // Remove previous header image if any
  406. if tab.image != nil {
  407. tab.header.Remove(tab.image)
  408. tab.image.Dispose()
  409. tab.image = nil
  410. }
  411. // Creates or updates icon
  412. if tab.icon == nil {
  413. tab.icon = NewIcon(icon)
  414. tab.icon.SetPaddingsFrom(&tab.styles.IconPaddings)
  415. tab.header.Add(tab.icon)
  416. } else {
  417. tab.icon.SetText(icon)
  418. }
  419. // Needs to recalculate all Tabs because this Tab width will change
  420. tab.tb.recalc()
  421. return tab
  422. }
  423. // SetImage sets the optional image of the Tab header
  424. func (tab *Tab) SetImage(imgfile string) error {
  425. // Remove previous icon if any
  426. if tab.icon != nil {
  427. tab.header.Remove(tab.icon)
  428. tab.icon.Dispose()
  429. tab.icon = nil
  430. }
  431. // Creates or updates image
  432. if tab.image == nil {
  433. // Creates image panel from file
  434. img, err := NewImage(imgfile)
  435. if err != nil {
  436. return err
  437. }
  438. tab.image = img
  439. tab.image.SetPaddingsFrom(&tab.styles.ImagePaddings)
  440. tab.header.Add(tab.image)
  441. } else {
  442. err := tab.image.SetImage(imgfile)
  443. if err != nil {
  444. return err
  445. }
  446. }
  447. // Scale image so its height is not greater than the Label height
  448. if tab.image.Height() > tab.label.Height() {
  449. tab.image.SetContentAspectHeight(tab.label.Height())
  450. }
  451. // Needs to recalculate all Tabs because this Tab width will change
  452. tab.tb.recalc()
  453. return nil
  454. }
  455. // SetPinned sets the tab pinned state.
  456. // A pinned tab cannot be removed by the user because the close icon is not shown.
  457. func (tab *Tab) SetPinned(pinned bool) {
  458. tab.pinned = pinned
  459. tab.iconClose.SetVisible(!pinned)
  460. }
  461. // Pinned returns this tab pinned state
  462. func (tab *Tab) Pinned() bool {
  463. return tab.pinned
  464. }
  465. // Header returns a pointer to this Tab header panel.
  466. // Can be used to set an event handler when the Tab header is right clicked.
  467. // (to show a context Menu for example).
  468. func (tab *Tab) Header() *Panel {
  469. return &tab.header
  470. }
  471. // SetContent sets or replaces this tab content panel.
  472. func (tab *Tab) SetContent(ipan IPanel) {
  473. // Remove previous content if any
  474. if tab.content != nil {
  475. tab.tb.Remove(tab.content)
  476. }
  477. tab.content = ipan
  478. if ipan != nil {
  479. tab.tb.Add(tab.content)
  480. }
  481. tab.tb.recalc()
  482. }
  483. // Content returns a pointer to the specified Tab content panel
  484. func (tab *Tab) Content() IPanel {
  485. return tab.content
  486. }
  487. // setSelected sets this Tab selected state
  488. func (tab *Tab) setSelected(selected bool) {
  489. tab.selected = selected
  490. if tab.content != nil {
  491. tab.content.GetPanel().SetVisible(selected)
  492. }
  493. tab.bottom.SetVisible(selected)
  494. tab.update()
  495. tab.setBottomPanel()
  496. }
  497. // minWidth returns the minimum width of this Tab header to allow
  498. // all of its elements to be shown in full.
  499. func (tab *Tab) minWidth() float32 {
  500. var minWidth float32
  501. if tab.icon != nil {
  502. minWidth = tab.icon.Width()
  503. } else if tab.image != nil {
  504. minWidth = tab.image.Width()
  505. }
  506. minWidth += tab.label.Width()
  507. minWidth += tab.iconClose.Width()
  508. return minWidth + tab.header.MinWidth()
  509. }
  510. // applyStyle applies the specified Tab style to the Tab header
  511. func (tab *Tab) applyStyle(s *TabStyle) {
  512. tab.header.GetPanel().ApplyStyle(&s.PanelStyle)
  513. }
  514. // update updates the Tab header visual style
  515. func (tab *Tab) update() {
  516. if !tab.header.Enabled() {
  517. tab.applyStyle(&tab.styles.Disabled)
  518. return
  519. }
  520. if tab.selected {
  521. tab.applyStyle(&tab.styles.Selected)
  522. return
  523. }
  524. if tab.cursorOver {
  525. tab.applyStyle(&tab.styles.Over)
  526. return
  527. }
  528. tab.applyStyle(&tab.styles.Normal)
  529. }
  530. // setBottomPanel sets the position and size of the Tab bottom panel
  531. // to cover the Tabs separator
  532. func (tab *Tab) setBottomPanel() {
  533. if tab.selected {
  534. bwidth := tab.header.ContentWidth() + tab.header.Paddings().Left + tab.header.Paddings().Right
  535. bx := tab.styles.Selected.Margin.Left + tab.styles.Selected.Border.Left
  536. tab.bottom.SetSize(bwidth, tab.tb.styles.SepHeight)
  537. tab.bottom.SetPosition(bx, tab.header.Height())
  538. }
  539. }
  540. // recalc recalculates the size of the Tab header and the size
  541. // and positions of the Tab header internal panels
  542. func (tab *Tab) recalc(width float32) {
  543. height := tab.label.Height()
  544. tab.header.SetContentHeight(height)
  545. tab.header.SetWidth(width)
  546. labx := float32(0)
  547. if tab.icon != nil {
  548. icy := (tab.header.ContentHeight() - tab.icon.Height()) / 2
  549. tab.icon.SetPosition(0, icy)
  550. labx = tab.icon.Width()
  551. } else if tab.image != nil {
  552. tab.image.SetPosition(0, 0)
  553. labx = tab.image.Width()
  554. }
  555. tab.label.SetPosition(labx, 0)
  556. // Sets the close icon position
  557. icx := tab.header.ContentWidth() - tab.iconClose.Width()
  558. icy := (tab.header.ContentHeight() - tab.iconClose.Height()) / 2
  559. tab.iconClose.SetPosition(icx, icy)
  560. // Sets the position of the bottom panel to cover separator
  561. tab.setBottomPanel()
  562. }