tabbar.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672
  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(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(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(OnMouseOut, 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. tb.root.StopPropagation(StopAll)
  212. }
  213. // onListButtonMouse process subscribed MouseButton events over the list button
  214. func (tb *TabBar) onListButton(evname string, ev interface{}) {
  215. switch evname {
  216. case OnMouseDown:
  217. if !tb.list.Visible() {
  218. tb.list.SetVisible(true)
  219. }
  220. default:
  221. return
  222. }
  223. tb.root.StopPropagation(StopAll)
  224. }
  225. // onListChange process OnChange event from the tab list
  226. func (tb *TabBar) onListChange(evname string, ev interface{}) {
  227. selected := tb.list.Selected()
  228. pos := selected[0].GetPanel().UserData().(int)
  229. log.Error("onListChange:%v", pos)
  230. tb.SetSelected(pos)
  231. tb.list.SetVisible(false)
  232. }
  233. // applyStyle applies the specified TabBar style
  234. func (tb *TabBar) applyStyle(s *TabBarStyle) {
  235. tb.Panel.ApplyStyle(&s.PanelStyle)
  236. tb.separator.SetColor4(&s.BorderColor)
  237. }
  238. // recalc recalculates and updates the positions of all tabs
  239. func (tb *TabBar) recalc() {
  240. // Determines how many tabs could be fully shown
  241. iconWidth := tb.listButton.Width()
  242. availWidth := tb.ContentWidth() - iconWidth
  243. var tabWidth float32
  244. var totalWidth float32
  245. var count int
  246. for i := 0; i < len(tb.tabs); i++ {
  247. tab := tb.tabs[i]
  248. minw := tab.minWidth()
  249. if minw > tabWidth {
  250. tabWidth = minw
  251. }
  252. totalWidth = float32(count+1) * tabWidth
  253. if totalWidth > availWidth {
  254. break
  255. }
  256. count++
  257. }
  258. // If there are more Tabs that can be shown, shows list button
  259. if count < len(tb.tabs) {
  260. // Sets the list button visible
  261. tb.listButton.SetVisible(true)
  262. height := tb.tabs[0].header.Height()
  263. iy := (height - tb.listButton.Height()) / 2
  264. tb.listButton.SetPosition(availWidth, iy)
  265. // Sets the tab list position and size
  266. listWidth := float32(200)
  267. lx := tb.ContentWidth() - listWidth
  268. ly := height + 1
  269. tb.list.SetPosition(lx, ly)
  270. tb.list.SetSize(listWidth, 200)
  271. tb.SetTopChild(tb.list)
  272. } else {
  273. tb.listButton.SetVisible(false)
  274. tb.list.SetVisible(false)
  275. }
  276. tb.list.Clear()
  277. var headerx float32
  278. // When there is available space limits the with of the tabs
  279. maxTabWidth := availWidth / float32(count)
  280. if tabWidth < maxTabWidth {
  281. tabWidth += (maxTabWidth - tabWidth) / 4
  282. }
  283. for i := 0; i < len(tb.tabs); i++ {
  284. tab := tb.tabs[i]
  285. // Recalculate Tab header and sets its position
  286. tab.recalc(tabWidth)
  287. tab.header.SetPosition(headerx, 0)
  288. // Sets size and position of the Tab content panel
  289. if tab.content != nil {
  290. cpan := tab.content.GetPanel()
  291. contenty := tab.header.Height() + tb.styles.SepHeight
  292. cpan.SetWidth(tb.ContentWidth())
  293. cpan.SetHeight(tb.ContentHeight() - contenty)
  294. cpan.SetPosition(0, contenty)
  295. }
  296. headerx += tab.header.Width()
  297. // If Tab can be shown set its header visible
  298. if i < count {
  299. tab.header.SetVisible(true)
  300. // Otherwise insert tab text in List
  301. } else {
  302. tab.header.SetVisible(false)
  303. item := NewImageLabel(tab.label.Text())
  304. item.SetUserData(i)
  305. tb.list.Add(item)
  306. }
  307. }
  308. // Sets the separator size, position and visibility
  309. if len(tb.tabs) > 0 {
  310. tb.separator.SetSize(tb.ContentWidth(), tb.styles.SepHeight)
  311. tb.separator.SetPositionY(tb.tabs[0].header.Height())
  312. tb.separator.SetVisible(true)
  313. } else {
  314. tb.separator.SetVisible(false)
  315. }
  316. }
  317. // update updates the TabBar visual state
  318. func (tb *TabBar) update() {
  319. if !tb.Enabled() {
  320. tb.applyStyle(&tb.styles.Disabled)
  321. return
  322. }
  323. if tb.cursorOver {
  324. tb.applyStyle(&tb.styles.Over)
  325. return
  326. }
  327. tb.applyStyle(&tb.styles.Normal)
  328. }
  329. //
  330. // Tab describes an individual tab of the TabBar
  331. //
  332. type Tab struct {
  333. tb *TabBar // Pointer to parent *TabBar
  334. styles *TabStyles // Pointer to Tab current styles
  335. header Panel // Tab header
  336. label *Label // Tab user label
  337. iconClose *Label // Tab close icon
  338. icon *Label // Tab optional user icon
  339. image *Image // Tab optional user image
  340. bottom Panel // Panel to cover the bottom edge of the Tab
  341. content IPanel // User content panel
  342. cursorOver bool
  343. selected bool
  344. pinned bool
  345. }
  346. // newTab creates and returns a pointer to a new Tab
  347. func newTab(text string, tb *TabBar, styles *TabStyles) *Tab {
  348. tab := new(Tab)
  349. tab.tb = tb
  350. tab.styles = styles
  351. // Setup the header panel
  352. tab.header.Initialize(0, 0)
  353. tab.label = NewLabel(text)
  354. tab.iconClose = NewIcon(styles.IconClose)
  355. tab.header.Add(tab.label)
  356. tab.header.Add(tab.iconClose)
  357. // Creates the bottom panel
  358. tab.bottom.Initialize(0, 0)
  359. tab.bottom.SetBounded(false)
  360. tab.bottom.SetColor4(&tab.styles.Selected.BgColor)
  361. tab.header.Add(&tab.bottom)
  362. // Subscribe to header panel events
  363. tab.header.Subscribe(OnCursorEnter, tab.onCursor)
  364. tab.header.Subscribe(OnCursorLeave, tab.onCursor)
  365. tab.header.Subscribe(OnMouseDown, tab.onMouseHeader)
  366. tab.iconClose.Subscribe(OnMouseDown, tab.onMouseIcon)
  367. tab.update()
  368. return tab
  369. }
  370. // onCursor process subscribed cursor events over the tab header
  371. func (tab *Tab) onCursor(evname string, ev interface{}) {
  372. switch evname {
  373. case OnCursorEnter:
  374. tab.cursorOver = true
  375. tab.update()
  376. case OnCursorLeave:
  377. tab.cursorOver = false
  378. tab.update()
  379. default:
  380. return
  381. }
  382. tab.header.root.StopPropagation(StopAll)
  383. }
  384. // onMouse process subscribed mouse events over the tab header
  385. func (tab *Tab) onMouseHeader(evname string, ev interface{}) {
  386. switch evname {
  387. case OnMouseDown:
  388. mev := ev.(*window.MouseEvent)
  389. if mev.Button == window.MouseButtonLeft {
  390. tab.tb.SetSelected(tab.tb.TabPosition(tab))
  391. } else {
  392. tab.header.Dispatch(OnRightClick, ev)
  393. }
  394. default:
  395. return
  396. }
  397. tab.header.root.StopPropagation(StopAll)
  398. }
  399. // onMouseIcon process subscribed mouse events over the tab close icon
  400. func (tab *Tab) onMouseIcon(evname string, ev interface{}) {
  401. switch evname {
  402. case OnMouseDown:
  403. tab.tb.RemoveTab(tab.tb.TabPosition(tab))
  404. default:
  405. return
  406. }
  407. tab.header.root.StopPropagation(StopAll)
  408. }
  409. // SetText sets the text of the tab header
  410. func (tab *Tab) SetText(text string) *Tab {
  411. tab.label.SetText(text)
  412. // Needs to recalculate all Tabs because this Tab width will change
  413. tab.tb.recalc()
  414. return tab
  415. }
  416. // SetIcon sets the optional icon of the Tab header
  417. func (tab *Tab) SetIcon(icon string) *Tab {
  418. // Remove previous header image if any
  419. if tab.image != nil {
  420. tab.header.Remove(tab.image)
  421. tab.image.Dispose()
  422. tab.image = nil
  423. }
  424. // Creates or updates icon
  425. if tab.icon == nil {
  426. tab.icon = NewIcon(icon)
  427. tab.icon.SetPaddingsFrom(&tab.styles.IconPaddings)
  428. tab.header.Add(tab.icon)
  429. } else {
  430. tab.icon.SetText(icon)
  431. }
  432. // Needs to recalculate all Tabs because this Tab width will change
  433. tab.tb.recalc()
  434. return tab
  435. }
  436. // SetImage sets the optional image of the Tab header
  437. func (tab *Tab) SetImage(imgfile string) error {
  438. // Remove previous icon if any
  439. if tab.icon != nil {
  440. tab.header.Remove(tab.icon)
  441. tab.icon.Dispose()
  442. tab.icon = nil
  443. }
  444. // Creates or updates image
  445. if tab.image == nil {
  446. // Creates image panel from file
  447. img, err := NewImage(imgfile)
  448. if err != nil {
  449. return err
  450. }
  451. tab.image = img
  452. tab.image.SetPaddingsFrom(&tab.styles.ImagePaddings)
  453. tab.header.Add(tab.image)
  454. } else {
  455. err := tab.image.SetImage(imgfile)
  456. if err != nil {
  457. return err
  458. }
  459. }
  460. // Scale image so its height is not greater than the Label height
  461. if tab.image.Height() > tab.label.Height() {
  462. tab.image.SetContentAspectHeight(tab.label.Height())
  463. }
  464. // Needs to recalculate all Tabs because this Tab width will change
  465. tab.tb.recalc()
  466. return nil
  467. }
  468. // SetPinned sets the tab pinned state.
  469. // A pinned tab cannot be removed by the user because the close icon is not shown.
  470. func (tab *Tab) SetPinned(pinned bool) {
  471. tab.pinned = pinned
  472. tab.iconClose.SetVisible(!pinned)
  473. }
  474. // Pinned returns this tab pinned state
  475. func (tab *Tab) Pinned() bool {
  476. return tab.pinned
  477. }
  478. // Header returns a pointer to this Tab header panel.
  479. // Can be used to set an event handler when the Tab header is right clicked.
  480. // (to show a context Menu for example).
  481. func (tab *Tab) Header() *Panel {
  482. return &tab.header
  483. }
  484. // SetContent sets or replaces this tab content panel.
  485. func (tab *Tab) SetContent(ipan IPanel) {
  486. // Remove previous content if any
  487. if tab.content != nil {
  488. tab.tb.Remove(tab.content)
  489. }
  490. tab.content = ipan
  491. if ipan != nil {
  492. tab.tb.Add(tab.content)
  493. }
  494. tab.tb.recalc()
  495. }
  496. // Content returns a pointer to the specified Tab content panel
  497. func (tab *Tab) Content() IPanel {
  498. return tab.content
  499. }
  500. // setSelected sets this Tab selected state
  501. func (tab *Tab) setSelected(selected bool) {
  502. tab.selected = selected
  503. if tab.content != nil {
  504. tab.content.GetPanel().SetVisible(selected)
  505. }
  506. tab.bottom.SetVisible(selected)
  507. tab.update()
  508. tab.setBottomPanel()
  509. }
  510. // minWidth returns the minimum width of this Tab header to allow
  511. // all of its elements to be shown in full.
  512. func (tab *Tab) minWidth() float32 {
  513. var minWidth float32
  514. if tab.icon != nil {
  515. minWidth = tab.icon.Width()
  516. } else if tab.image != nil {
  517. minWidth = tab.image.Width()
  518. }
  519. minWidth += tab.label.Width()
  520. minWidth += tab.iconClose.Width()
  521. return minWidth + tab.header.MinWidth()
  522. }
  523. // applyStyle applies the specified Tab style to the Tab header
  524. func (tab *Tab) applyStyle(s *TabStyle) {
  525. tab.header.GetPanel().ApplyStyle(&s.PanelStyle)
  526. }
  527. // update updates the Tab header visual style
  528. func (tab *Tab) update() {
  529. if !tab.header.Enabled() {
  530. tab.applyStyle(&tab.styles.Disabled)
  531. return
  532. }
  533. if tab.selected {
  534. tab.applyStyle(&tab.styles.Selected)
  535. return
  536. }
  537. if tab.cursorOver {
  538. tab.applyStyle(&tab.styles.Over)
  539. return
  540. }
  541. tab.applyStyle(&tab.styles.Normal)
  542. }
  543. // setBottomPanel sets the position and size of the Tab bottom panel
  544. // to cover the Tabs separator
  545. func (tab *Tab) setBottomPanel() {
  546. if tab.selected {
  547. bwidth := tab.header.ContentWidth() + tab.header.Paddings().Left + tab.header.Paddings().Right
  548. bx := tab.styles.Selected.Margin.Left + tab.styles.Selected.Border.Left
  549. tab.bottom.SetSize(bwidth, tab.tb.styles.SepHeight)
  550. tab.bottom.SetPosition(bx, tab.header.Height())
  551. }
  552. }
  553. // recalc recalculates the size of the Tab header and the size
  554. // and positions of the Tab header internal panels
  555. func (tab *Tab) recalc(width float32) {
  556. height := tab.label.Height()
  557. tab.header.SetContentHeight(height)
  558. tab.header.SetWidth(width)
  559. labx := float32(0)
  560. if tab.icon != nil {
  561. icy := (tab.header.ContentHeight() - tab.icon.Height()) / 2
  562. tab.icon.SetPosition(0, icy)
  563. labx = tab.icon.Width()
  564. } else if tab.image != nil {
  565. tab.image.SetPosition(0, 0)
  566. labx = tab.image.Width()
  567. }
  568. tab.label.SetPosition(labx, 0)
  569. // Sets the close icon position
  570. icx := tab.header.ContentWidth() - tab.iconClose.Width()
  571. icy := (tab.header.ContentHeight() - tab.iconClose.Height()) / 2
  572. tab.iconClose.SetPosition(icx, icy)
  573. // Sets the position of the bottom panel to cover separator
  574. tab.setBottomPanel()
  575. }