| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692 |
- // Copyright 2016 The G3N Authors. All rights reserved.
- // Use of this source code is governed by a BSD-style
- // license that can be found in the LICENSE file.
- package gui
- import (
- "fmt"
- "github.com/g3n/engine/math32"
- "github.com/g3n/engine/window"
- )
- // TabBar is a panel which can contain other panels arranged in horizontal Tabs.
- // Only one panel is visible at a time.
- // To show another panel the corresponding Tab must be selected.
- type TabBar struct {
- Panel // Embedded panel
- styles *TabBarStyles // Pointer to current styles
- tabs []*Tab // Array of tabs
- separator Panel // Separator Panel
- listButton *Label // Icon for tab list button
- list *List // List for not visible tabs
- selected int // Index of the selected tab
- cursorOver bool // Cursor over TabBar panel flag
- }
- // TabBarStyle describes the style of the TabBar
- type TabBarStyle struct {
- Border BorderSizes // Border sizes
- Paddings BorderSizes // Padding sizes
- BorderColor math32.Color4 // Border color
- BgColor math32.Color4 // Background color
- }
- // TabBarStyles describes all the TabBarStyles
- type TabBarStyles struct {
- SepHeight float32 // Separator width
- ListButtonIcon string // Icon for list button
- ListButtonPaddings BorderSizes // Paddings for list button
- Normal TabBarStyle // Style for normal exhibition
- Over TabBarStyle // Style when cursor is over the TabBar
- Focus TabBarStyle // Style when the TabBar has key focus
- Disabled TabBarStyle // Style when the TabBar is disabled
- Tab TabStyles // Style for Tabs
- }
- // TabStyle describes the style of the individual Tabs header
- type TabStyle struct {
- Margins BorderSizes // Tab header margins
- Border BorderSizes // Tab header borders
- Paddings BorderSizes // Tab header paddings
- BorderColor math32.Color4 // Tab header border color
- BgColor math32.Color4 // Tab header background color
- FgColor math32.Color // Tab header color for icon and text
- }
- // TabStyles describes all Tab styles
- type TabStyles struct {
- IconPaddings BorderSizes // Paddings for optional icon
- ImagePaddings BorderSizes // Paddings for optional image
- IconClose string // Codepoint for close icon in Tab header
- Normal TabStyle // Style for normal exhibition
- Over TabStyle // Style when cursor is over the Tab
- Focus TabStyle // Style when the Tab has key focus
- Disabled TabStyle // Style when the Tab is disabled
- Selected TabStyle // Style when the Tab is selected
- }
- // NewTabBar creates and returns a pointer to a new TabBar widget
- // with the specified width and height
- func NewTabBar(width, height float32) *TabBar {
- // Creates new TabBar
- tb := new(TabBar)
- tb.Initialize(width, height)
- tb.styles = &StyleDefault().TabBar
- tb.tabs = make([]*Tab, 0)
- tb.selected = -1
- // Creates separator panel (between the tab headers and content panel)
- tb.separator.Initialize(0, 0)
- tb.Add(&tb.separator)
- // Create list for contained tabs not visible
- tb.list = NewVList(0, 0)
- tb.list.Subscribe(OnMouseOut, func(evname string, ev interface{}) {
- tb.list.SetVisible(false)
- })
- tb.list.Subscribe(OnChange, tb.onListChange)
- tb.Add(tb.list)
- // Creates list icon button
- tb.listButton = NewLabel(tb.styles.ListButtonIcon, true)
- tb.listButton.SetPaddingsFrom(&tb.styles.ListButtonPaddings)
- tb.listButton.Subscribe(OnMouseDown, tb.onListButton)
- tb.Add(tb.listButton)
- // Subscribe to panel events
- tb.Subscribe(OnCursorEnter, tb.onCursor)
- tb.Subscribe(OnCursorLeave, tb.onCursor)
- tb.Subscribe(OnEnable, func(name string, ev interface{}) { tb.update() })
- tb.Subscribe(OnResize, func(name string, ev interface{}) { tb.recalc() })
- tb.recalc()
- tb.update()
- return tb
- }
- // AddTab creates and adds a new Tab panel with the specified header text
- // at the end of this TabBar list of tabs.
- // Returns the pointer to thew new Tab.
- func (tb *TabBar) AddTab(text string) *Tab {
- tab := tb.InsertTab(text, len(tb.tabs))
- tb.SetSelected(len(tb.tabs) - 1)
- return tab
- }
- // InsertTab creates and inserts a new Tab panel with the specified header text
- // at the specified position in the TabBar from left to right.
- // Returns the pointer to the new Tab or nil if the position is invalid.
- func (tb *TabBar) InsertTab(text string, pos int) *Tab {
- // Checks position to insert into
- if pos < 0 || pos > len(tb.tabs) {
- return nil
- }
- // Inserts created Tab at the specified position
- tab := newTab(text, tb, &tb.styles.Tab)
- tb.tabs = append(tb.tabs, nil)
- copy(tb.tabs[pos+1:], tb.tabs[pos:])
- tb.tabs[pos] = tab
- tb.Add(&tab.header)
- tb.update()
- tb.recalc()
- return tab
- }
- // RemoveTab removes the tab at the specified position in the TabBar.
- // Returns an error if the position is invalid.
- func (tb *TabBar) RemoveTab(pos int) error {
- // Check position to remove from
- if pos < 0 || pos >= len(tb.tabs) {
- return fmt.Errorf("Invalid tab position:%d", pos)
- }
- // Remove tab from TabBar panel
- tab := tb.tabs[pos]
- tb.Remove(&tab.header)
- if tab.content != nil {
- tb.Remove(tab.content)
- }
- // Remove tab from tabbar array
- copy(tb.tabs[pos:], tb.tabs[pos+1:])
- tb.tabs[len(tb.tabs)-1] = nil
- tb.tabs = tb.tabs[:len(tb.tabs)-1]
- // If removed tab was selected, selects other tab.
- if tb.selected == pos {
- // Try to select tab at right
- if len(tb.tabs) > pos {
- tb.tabs[pos].setSelected(true)
- // Otherwise select tab at left
- } else if pos > 0 {
- tb.tabs[pos-1].setSelected(true)
- }
- }
- tb.update()
- tb.recalc()
- return nil
- }
- // MoveTab moves a Tab to another position in the Tabs list
- func (tb *TabBar) MoveTab(src, dest int) error {
- // Check source position
- if src < 0 || src >= len(tb.tabs) {
- return fmt.Errorf("Invalid tab source position:%d", src)
- }
- // Check destination position
- if dest < 0 || dest >= len(tb.tabs) {
- return fmt.Errorf("Invalid tab destination position:%d", dest)
- }
- if src == dest {
- return nil
- }
- tabDest := tb.tabs[dest]
- tb.tabs[dest] = tb.tabs[src]
- tb.tabs[src] = tabDest
- tb.recalc()
- return nil
- }
- // TabCount returns the current number of Tabs in the TabBar
- func (tb *TabBar) TabCount() int {
- return len(tb.tabs)
- }
- // TabAt returns the pointer of the Tab object at the specified position.
- // Return nil if the position is invalid
- func (tb *TabBar) TabAt(pos int) *Tab {
- if pos < 0 || pos >= len(tb.tabs) {
- return nil
- }
- return tb.tabs[pos]
- }
- // TabPosition returns the position of the Tab specified by its pointer
- func (tb *TabBar) TabPosition(tab *Tab) int {
- for i := 0; i < len(tb.tabs); i++ {
- if tb.tabs[i] == tab {
- return i
- }
- }
- return -1
- }
- // SetSelected sets the selected tab of the TabBar to the tab with the specified position.
- // Returns the pointer of the selected tab or nil if the position is invalid.
- func (tb *TabBar) SetSelected(pos int) *Tab {
- if pos < 0 || pos >= len(tb.tabs) {
- return nil
- }
- for i := 0; i < len(tb.tabs); i++ {
- if i == pos {
- tb.tabs[i].setSelected(true)
- } else {
- tb.tabs[i].setSelected(false)
- }
- }
- tb.selected = pos
- return tb.tabs[pos]
- }
- // Selected returns the position of the selected Tab.
- // Returns value < 0 if there is no selected Tab.
- func (tb *TabBar) Selected() int {
- return tb.selected
- }
- // onCursor process subscribed cursor events
- func (tb *TabBar) onCursor(evname string, ev interface{}) {
- switch evname {
- case OnCursorEnter:
- tb.cursorOver = true
- tb.update()
- case OnCursorLeave:
- tb.cursorOver = false
- tb.update()
- default:
- return
- }
- tb.root.StopPropagation(StopAll)
- }
- // onListButtonMouse process subscribed MouseButton events over the list button
- func (tb *TabBar) onListButton(evname string, ev interface{}) {
- switch evname {
- case OnMouseDown:
- if !tb.list.Visible() {
- tb.list.SetVisible(true)
- }
- default:
- return
- }
- tb.root.StopPropagation(StopAll)
- }
- // onListChange process OnChange event from the tab list
- func (tb *TabBar) onListChange(evname string, ev interface{}) {
- selected := tb.list.Selected()
- pos := selected[0].GetPanel().UserData().(int)
- log.Error("onListChange:%v", pos)
- tb.SetSelected(pos)
- tb.list.SetVisible(false)
- }
- // applyStyle applies the specified TabBar style
- func (tb *TabBar) applyStyle(s *TabBarStyle) {
- tb.SetBordersFrom(&s.Border)
- tb.SetBordersColor4(&s.BorderColor)
- tb.SetPaddingsFrom(&s.Paddings)
- tb.SetColor4(&s.BgColor)
- tb.separator.SetColor4(&s.BorderColor)
- }
- // recalc recalculates and updates the positions of all tabs
- func (tb *TabBar) recalc() {
- // Determines how many tabs could be fully shown
- iconWidth := tb.listButton.Width()
- availWidth := tb.ContentWidth() - iconWidth
- var tabWidth float32
- var totalWidth float32
- var count int
- for i := 0; i < len(tb.tabs); i++ {
- tab := tb.tabs[i]
- minw := tab.minWidth()
- if minw > tabWidth {
- tabWidth = minw
- }
- totalWidth = float32(count+1) * tabWidth
- if totalWidth > availWidth {
- break
- }
- count++
- }
- // If there are more Tabs that can be shown, shows list button
- if count < len(tb.tabs) {
- // Sets the list button visible
- tb.listButton.SetVisible(true)
- height := tb.tabs[0].header.Height()
- iy := (height - tb.listButton.Height()) / 2
- tb.listButton.SetPosition(availWidth, iy)
- // Sets the tab list position and size
- listWidth := float32(200)
- lx := tb.ContentWidth() - listWidth
- ly := height + 1
- tb.list.SetPosition(lx, ly)
- tb.list.SetSize(listWidth, 200)
- tb.SetTopChild(tb.list)
- } else {
- tb.listButton.SetVisible(false)
- tb.list.SetVisible(false)
- }
- tb.list.Clear()
- var headerx float32
- // When there is available space limits the with of the tabs
- maxTabWidth := availWidth / float32(count)
- if tabWidth < maxTabWidth {
- tabWidth += (maxTabWidth - tabWidth) / 4
- }
- for i := 0; i < len(tb.tabs); i++ {
- tab := tb.tabs[i]
- // Recalculate Tab header and sets its position
- tab.recalc(tabWidth)
- tab.header.SetPosition(headerx, 0)
- // Sets size and position of the Tab content panel
- if tab.content != nil {
- cpan := tab.content.GetPanel()
- contenty := tab.header.Height() + tb.styles.SepHeight
- cpan.SetWidth(tb.ContentWidth())
- cpan.SetHeight(tb.ContentHeight() - contenty)
- cpan.SetPosition(0, contenty)
- }
- headerx += tab.header.Width()
- // If Tab can be shown set its header visible
- if i < count {
- tab.header.SetVisible(true)
- // Otherwise insert tab text in List
- } else {
- tab.header.SetVisible(false)
- item := NewImageLabel(tab.label.Text())
- item.SetUserData(i)
- tb.list.Add(item)
- }
- }
- // Sets the separator size, position and visibility
- if len(tb.tabs) > 0 {
- tb.separator.SetSize(tb.ContentWidth(), tb.styles.SepHeight)
- tb.separator.SetPositionY(tb.tabs[0].header.Height())
- tb.separator.SetVisible(true)
- } else {
- tb.separator.SetVisible(false)
- }
- }
- // update updates the TabBar visual state
- func (tb *TabBar) update() {
- if !tb.Enabled() {
- tb.applyStyle(&tb.styles.Disabled)
- return
- }
- if tb.cursorOver {
- tb.applyStyle(&tb.styles.Over)
- return
- }
- tb.applyStyle(&tb.styles.Normal)
- }
- //
- // Tab describes an individual tab of the TabBar
- //
- type Tab struct {
- tb *TabBar // Pointer to parent *TabBar
- styles *TabStyles // Pointer to Tab current styles
- header Panel // Tab header
- label *Label // Tab user label
- iconClose *Label // Tab close icon
- icon *Label // Tab optional user icon
- image *Image // Tab optional user image
- bottom Panel // Panel to cover the bottom edge of the Tab
- content IPanel // User content panel
- cursorOver bool
- selected bool
- pinned bool
- }
- // newTab creates and returns a pointer to a new Tab
- func newTab(text string, tb *TabBar, styles *TabStyles) *Tab {
- tab := new(Tab)
- tab.tb = tb
- tab.styles = styles
- // Setup the header panel
- tab.header.Initialize(0, 0)
- tab.label = NewLabel(text)
- tab.iconClose = NewLabel(styles.IconClose, true)
- tab.header.Add(tab.label)
- tab.header.Add(tab.iconClose)
- // Creates the bottom panel
- tab.bottom.Initialize(0, 0)
- tab.bottom.SetBounded(false)
- tab.bottom.SetColor4(&tab.styles.Selected.BgColor)
- tab.header.Add(&tab.bottom)
- // Subscribe to header panel events
- tab.header.Subscribe(OnCursorEnter, tab.onCursor)
- tab.header.Subscribe(OnCursorLeave, tab.onCursor)
- tab.header.Subscribe(OnMouseDown, tab.onMouseHeader)
- tab.iconClose.Subscribe(OnMouseDown, tab.onMouseIcon)
- tab.update()
- return tab
- }
- // onCursor process subscribed cursor events over the tab header
- func (tab *Tab) onCursor(evname string, ev interface{}) {
- switch evname {
- case OnCursorEnter:
- tab.cursorOver = true
- tab.update()
- case OnCursorLeave:
- tab.cursorOver = false
- tab.update()
- default:
- return
- }
- tab.header.root.StopPropagation(StopAll)
- }
- // onMouse process subscribed mouse events over the tab header
- func (tab *Tab) onMouseHeader(evname string, ev interface{}) {
- switch evname {
- case OnMouseDown:
- mev := ev.(*window.MouseEvent)
- if mev.Button == window.MouseButtonLeft {
- tab.tb.SetSelected(tab.tb.TabPosition(tab))
- } else {
- tab.header.Dispatch(OnRightClick, ev)
- }
- default:
- return
- }
- tab.header.root.StopPropagation(StopAll)
- }
- // onMouseIcon process subscribed mouse events over the tab close icon
- func (tab *Tab) onMouseIcon(evname string, ev interface{}) {
- switch evname {
- case OnMouseDown:
- tab.tb.RemoveTab(tab.tb.TabPosition(tab))
- default:
- return
- }
- tab.header.root.StopPropagation(StopAll)
- }
- // SetText sets the text of the tab header
- func (tab *Tab) SetText(text string) *Tab {
- tab.label.SetText(text)
- // Needs to recalculate all Tabs because this Tab width will change
- tab.tb.recalc()
- return tab
- }
- // SetIcon sets the optional icon of the Tab header
- func (tab *Tab) SetIcon(icon string) *Tab {
- // Remove previous header image if any
- if tab.image != nil {
- tab.header.Remove(tab.image)
- tab.image.Dispose()
- tab.image = nil
- }
- // Creates or updates icon
- if tab.icon == nil {
- tab.icon = NewLabel(icon, true)
- tab.icon.SetPaddingsFrom(&tab.styles.IconPaddings)
- tab.header.Add(tab.icon)
- } else {
- tab.icon.SetText(icon)
- }
- // Needs to recalculate all Tabs because this Tab width will change
- tab.tb.recalc()
- return tab
- }
- // SetImage sets the optional image of the Tab header
- func (tab *Tab) SetImage(imgfile string) error {
- // Remove previous icon if any
- if tab.icon != nil {
- tab.header.Remove(tab.icon)
- tab.icon.Dispose()
- tab.icon = nil
- }
- // Creates or updates image
- if tab.image == nil {
- // Creates image panel from file
- img, err := NewImage(imgfile)
- if err != nil {
- return err
- }
- tab.image = img
- tab.image.SetPaddingsFrom(&tab.styles.ImagePaddings)
- tab.header.Add(tab.image)
- } else {
- err := tab.image.SetImage(imgfile)
- if err != nil {
- return err
- }
- }
- // Scale image so its height is not greater than the Label height
- if tab.image.Height() > tab.label.Height() {
- tab.image.SetContentAspectHeight(tab.label.Height())
- }
- // Needs to recalculate all Tabs because this Tab width will change
- tab.tb.recalc()
- return nil
- }
- // SetPinned sets the tab pinned state.
- // A pinned tab cannot be removed by the user because the close icon is not shown.
- func (tab *Tab) SetPinned(pinned bool) {
- tab.pinned = pinned
- tab.iconClose.SetVisible(!pinned)
- }
- // Pinned returns this tab pinned state
- func (tab *Tab) Pinned() bool {
- return tab.pinned
- }
- // Header returns a pointer to this Tab header panel.
- // Can be used to set an event handler when the Tab header is right clicked.
- // (to show a context Menu for example).
- func (tab *Tab) Header() *Panel {
- return &tab.header
- }
- // SetContent sets or replaces this tab content panel.
- func (tab *Tab) SetContent(ipan IPanel) {
- // Remove previous content if any
- if tab.content != nil {
- tab.tb.Remove(tab.content)
- }
- tab.content = ipan
- if ipan != nil {
- tab.tb.Add(tab.content)
- }
- tab.tb.recalc()
- }
- // Content returns a pointer to the specified Tab content panel
- func (tab *Tab) Content() IPanel {
- return tab.content
- }
- // setSelected sets this Tab selected state
- func (tab *Tab) setSelected(selected bool) {
- tab.selected = selected
- if tab.content != nil {
- tab.content.GetPanel().SetVisible(selected)
- }
- tab.bottom.SetVisible(selected)
- tab.update()
- tab.setBottomPanel()
- }
- // minWidth returns the minimum width of this Tab header to allow
- // all of its elements to be shown in full.
- func (tab *Tab) minWidth() float32 {
- var minWidth float32
- if tab.icon != nil {
- minWidth = tab.icon.Width()
- } else if tab.image != nil {
- minWidth = tab.image.Width()
- }
- minWidth += tab.label.Width()
- minWidth += tab.iconClose.Width()
- return minWidth + tab.header.MinWidth()
- }
- // applyStyle applies the specified Tab style to the Tab header
- func (tab *Tab) applyStyle(s *TabStyle) {
- tab.header.SetMarginsFrom(&s.Margins)
- tab.header.SetBordersFrom(&s.Border)
- tab.header.SetBordersColor4(&s.BorderColor)
- tab.header.SetPaddingsFrom(&s.Paddings)
- tab.header.SetColor4(&s.BgColor)
- }
- // update updates the Tab header visual style
- func (tab *Tab) update() {
- if !tab.header.Enabled() {
- tab.applyStyle(&tab.styles.Disabled)
- return
- }
- if tab.selected {
- tab.applyStyle(&tab.styles.Selected)
- return
- }
- if tab.cursorOver {
- tab.applyStyle(&tab.styles.Over)
- return
- }
- tab.applyStyle(&tab.styles.Normal)
- }
- // setBottomPanel sets the position and size of the Tab bottom panel
- // to cover the Tabs separator
- func (tab *Tab) setBottomPanel() {
- if tab.selected {
- bwidth := tab.header.ContentWidth() + tab.header.Paddings().Left + tab.header.Paddings().Right
- bx := tab.styles.Selected.Margins.Left + tab.styles.Selected.Border.Left
- tab.bottom.SetSize(bwidth, tab.tb.styles.SepHeight)
- tab.bottom.SetPosition(bx, tab.header.Height())
- }
- }
- // recalc recalculates the size of the Tab header and the size
- // and positions of the Tab header internal panels
- func (tab *Tab) recalc(width float32) {
- height := tab.label.Height()
- tab.header.SetContentHeight(height)
- tab.header.SetWidth(width)
- labx := float32(0)
- if tab.icon != nil {
- icy := (tab.header.ContentHeight() - tab.icon.Height()) / 2
- tab.icon.SetPosition(0, icy)
- labx = tab.icon.Width()
- } else if tab.image != nil {
- tab.image.SetPosition(0, 0)
- labx = tab.image.Width()
- }
- tab.label.SetPosition(labx, 0)
- // Sets the close icon position
- icx := tab.header.ContentWidth() - tab.iconClose.Width()
- icy := (tab.header.ContentHeight() - tab.iconClose.Height()) / 2
- tab.iconClose.SetPosition(icx, icy)
- // Sets the position of the bottom panel to cover separator
- tab.setBottomPanel()
- }
|