464 lines
12 KiB
Go
464 lines
12 KiB
Go
package container
|
|
|
|
import (
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
// Declare conformity with Widget interface.
|
|
var _ fyne.Widget = (*AppTabs)(nil)
|
|
|
|
// AppTabs container is used to split your application into various different areas identified by tabs.
|
|
// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
|
|
// Each item is represented by a button at the edge of the container.
|
|
//
|
|
// Since: 1.4
|
|
type AppTabs struct {
|
|
widget.BaseWidget
|
|
|
|
Items []*TabItem
|
|
|
|
// Deprecated: Use `OnSelected func(*TabItem)` instead.
|
|
OnChanged func(*TabItem)
|
|
OnSelected func(*TabItem)
|
|
OnUnselected func(*TabItem)
|
|
|
|
current int
|
|
location TabLocation
|
|
isTransitioning bool
|
|
|
|
popUpMenu *widget.PopUpMenu
|
|
}
|
|
|
|
// NewAppTabs creates a new tab container that allows the user to choose between different areas of an app.
|
|
//
|
|
// Since: 1.4
|
|
func NewAppTabs(items ...*TabItem) *AppTabs {
|
|
tabs := &AppTabs{}
|
|
tabs.BaseWidget.ExtendBaseWidget(tabs)
|
|
tabs.SetItems(items)
|
|
return tabs
|
|
}
|
|
|
|
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
|
//
|
|
// Implements: fyne.Widget
|
|
func (t *AppTabs) CreateRenderer() fyne.WidgetRenderer {
|
|
t.BaseWidget.ExtendBaseWidget(t)
|
|
r := &appTabsRenderer{
|
|
baseTabsRenderer: baseTabsRenderer{
|
|
bar: &fyne.Container{},
|
|
divider: canvas.NewRectangle(theme.ShadowColor()),
|
|
indicator: canvas.NewRectangle(theme.PrimaryColor()),
|
|
},
|
|
appTabs: t,
|
|
}
|
|
r.action = r.buildOverflowTabsButton()
|
|
r.tabs = t
|
|
|
|
// Initially setup the tab bar to only show one tab, all others will be in overflow.
|
|
// When the widget is laid out, and we know the size, the tab bar will be updated to show as many as can fit.
|
|
r.updateTabs(1)
|
|
r.updateIndicator(false)
|
|
r.applyTheme(t)
|
|
return r
|
|
}
|
|
|
|
// Append adds a new TabItem to the end of the tab bar.
|
|
func (t *AppTabs) Append(item *TabItem) {
|
|
t.SetItems(append(t.Items, item))
|
|
}
|
|
|
|
// CurrentTab returns the currently selected TabItem.
|
|
//
|
|
// Deprecated: Use `AppTabs.Selected() *TabItem` instead.
|
|
func (t *AppTabs) CurrentTab() *TabItem {
|
|
if t.current < 0 || t.current >= len(t.Items) {
|
|
return nil
|
|
}
|
|
return t.Items[t.current]
|
|
}
|
|
|
|
// CurrentTabIndex returns the index of the currently selected TabItem.
|
|
//
|
|
// Deprecated: Use `AppTabs.SelectedIndex() int` instead.
|
|
func (t *AppTabs) CurrentTabIndex() int {
|
|
return t.current
|
|
}
|
|
|
|
// DisableIndex disables the TabItem at the specified index.
|
|
//
|
|
// Since: 2.3
|
|
func (t *AppTabs) DisableIndex(i int) {
|
|
disableIndex(t, i)
|
|
}
|
|
|
|
// DisableItem disables the specified TabItem.
|
|
//
|
|
// Since: 2.3
|
|
func (t *AppTabs) DisableItem(item *TabItem) {
|
|
disableItem(t, item)
|
|
}
|
|
|
|
// EnableIndex enables the TabItem at the specified index.
|
|
//
|
|
// Since: 2.3
|
|
func (t *AppTabs) EnableIndex(i int) {
|
|
enableIndex(t, i)
|
|
}
|
|
|
|
// EnableItem enables the specified TabItem.
|
|
//
|
|
// Since: 2.3
|
|
func (t *AppTabs) EnableItem(item *TabItem) {
|
|
enableItem(t, item)
|
|
}
|
|
|
|
// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
|
|
//
|
|
// Deprecated: Support for extending containers is being removed
|
|
func (t *AppTabs) ExtendBaseWidget(wid fyne.Widget) {
|
|
t.BaseWidget.ExtendBaseWidget(wid)
|
|
}
|
|
|
|
// Hide hides the widget.
|
|
//
|
|
// Implements: fyne.CanvasObject
|
|
func (t *AppTabs) Hide() {
|
|
if t.popUpMenu != nil {
|
|
t.popUpMenu.Hide()
|
|
t.popUpMenu = nil
|
|
}
|
|
t.BaseWidget.Hide()
|
|
}
|
|
|
|
// MinSize returns the size that this widget should not shrink below
|
|
//
|
|
// Implements: fyne.CanvasObject
|
|
func (t *AppTabs) MinSize() fyne.Size {
|
|
t.BaseWidget.ExtendBaseWidget(t)
|
|
return t.BaseWidget.MinSize()
|
|
}
|
|
|
|
// Remove tab by value.
|
|
func (t *AppTabs) Remove(item *TabItem) {
|
|
removeItem(t, item)
|
|
t.Refresh()
|
|
}
|
|
|
|
// RemoveIndex removes tab by index.
|
|
func (t *AppTabs) RemoveIndex(index int) {
|
|
removeIndex(t, index)
|
|
t.Refresh()
|
|
}
|
|
|
|
// Select sets the specified TabItem to be selected and its content visible.
|
|
func (t *AppTabs) Select(item *TabItem) {
|
|
selectItem(t, item)
|
|
t.Refresh()
|
|
}
|
|
|
|
// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
|
|
func (t *AppTabs) SelectIndex(index int) {
|
|
selectIndex(t, index)
|
|
t.Refresh()
|
|
}
|
|
|
|
// SelectTab sets the specified TabItem to be selected and its content visible.
|
|
//
|
|
// Deprecated: Use `AppTabs.Select(*TabItem)` instead.
|
|
func (t *AppTabs) SelectTab(item *TabItem) {
|
|
for i, child := range t.Items {
|
|
if child == item {
|
|
t.SelectTabIndex(i)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// SelectTabIndex sets the TabItem at the specific index to be selected and its content visible.
|
|
//
|
|
// Deprecated: Use `AppTabs.SelectIndex(int)` instead.
|
|
func (t *AppTabs) SelectTabIndex(index int) {
|
|
if index < 0 || index >= len(t.Items) || t.current == index {
|
|
return
|
|
}
|
|
t.current = index
|
|
t.Refresh()
|
|
|
|
if t.OnChanged != nil {
|
|
t.OnChanged(t.Items[t.current])
|
|
}
|
|
}
|
|
|
|
// Selected returns the currently selected TabItem.
|
|
func (t *AppTabs) Selected() *TabItem {
|
|
return selected(t)
|
|
}
|
|
|
|
// SelectedIndex returns the index of the currently selected TabItem.
|
|
func (t *AppTabs) SelectedIndex() int {
|
|
return t.current
|
|
}
|
|
|
|
// SetItems sets the containers items and refreshes.
|
|
func (t *AppTabs) SetItems(items []*TabItem) {
|
|
setItems(t, items)
|
|
t.Refresh()
|
|
}
|
|
|
|
// SetTabLocation sets the location of the tab bar
|
|
func (t *AppTabs) SetTabLocation(l TabLocation) {
|
|
t.location = tabsAdjustedLocation(l)
|
|
t.Refresh()
|
|
}
|
|
|
|
// Show this widget, if it was previously hidden
|
|
//
|
|
// Implements: fyne.CanvasObject
|
|
func (t *AppTabs) Show() {
|
|
t.BaseWidget.Show()
|
|
t.SelectIndex(t.current)
|
|
}
|
|
|
|
func (t *AppTabs) onUnselected() func(*TabItem) {
|
|
return t.OnUnselected
|
|
}
|
|
|
|
func (t *AppTabs) onSelected() func(*TabItem) {
|
|
return func(tab *TabItem) {
|
|
if f := t.OnChanged; f != nil {
|
|
f(tab)
|
|
}
|
|
if f := t.OnSelected; f != nil {
|
|
f(tab)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *AppTabs) items() []*TabItem {
|
|
return t.Items
|
|
}
|
|
|
|
func (t *AppTabs) selected() int {
|
|
return t.current
|
|
}
|
|
|
|
func (t *AppTabs) setItems(items []*TabItem) {
|
|
t.Items = items
|
|
}
|
|
|
|
func (t *AppTabs) setSelected(selected int) {
|
|
t.current = selected
|
|
}
|
|
|
|
func (t *AppTabs) setTransitioning(transitioning bool) {
|
|
t.isTransitioning = transitioning
|
|
}
|
|
|
|
func (t *AppTabs) tabLocation() TabLocation {
|
|
return t.location
|
|
}
|
|
|
|
func (t *AppTabs) transitioning() bool {
|
|
return t.isTransitioning
|
|
}
|
|
|
|
// Declare conformity with WidgetRenderer interface.
|
|
var _ fyne.WidgetRenderer = (*appTabsRenderer)(nil)
|
|
|
|
type appTabsRenderer struct {
|
|
baseTabsRenderer
|
|
appTabs *AppTabs
|
|
}
|
|
|
|
func (r *appTabsRenderer) Layout(size fyne.Size) {
|
|
// Try render as many tabs as will fit, others will appear in the overflow
|
|
if len(r.appTabs.Items) == 0 {
|
|
r.updateTabs(0)
|
|
} else {
|
|
for i := len(r.appTabs.Items); i > 0; i-- {
|
|
r.updateTabs(i)
|
|
barMin := r.bar.MinSize()
|
|
if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
|
|
if barMin.Height <= size.Height {
|
|
// Tab bar is short enough to fit
|
|
break
|
|
}
|
|
} else {
|
|
if barMin.Width <= size.Width {
|
|
// Tab bar is thin enough to fit
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
r.layout(r.appTabs, size)
|
|
r.updateIndicator(r.appTabs.transitioning())
|
|
if r.appTabs.transitioning() {
|
|
r.appTabs.setTransitioning(false)
|
|
}
|
|
}
|
|
|
|
func (r *appTabsRenderer) MinSize() fyne.Size {
|
|
return r.minSize(r.appTabs)
|
|
}
|
|
|
|
func (r *appTabsRenderer) Objects() []fyne.CanvasObject {
|
|
return r.objects(r.appTabs)
|
|
}
|
|
|
|
func (r *appTabsRenderer) Refresh() {
|
|
r.Layout(r.appTabs.Size())
|
|
|
|
r.refresh(r.appTabs)
|
|
|
|
canvas.Refresh(r.appTabs)
|
|
}
|
|
|
|
func (r *appTabsRenderer) buildOverflowTabsButton() (overflow *widget.Button) {
|
|
overflow = &widget.Button{Icon: moreIcon(r.appTabs), Importance: widget.LowImportance, OnTapped: func() {
|
|
// Show pop up containing all tabs which did not fit in the tab bar
|
|
|
|
itemLen, objLen := len(r.appTabs.Items), len(r.bar.Objects[0].(*fyne.Container).Objects)
|
|
items := make([]*fyne.MenuItem, 0, itemLen-objLen)
|
|
for i := objLen; i < itemLen; i++ {
|
|
index := i // capture
|
|
// FIXME MenuItem doesn't support icons (#1752)
|
|
// FIXME MenuItem can't show if it is the currently selected tab (#1753)
|
|
items = append(items, fyne.NewMenuItem(r.appTabs.Items[i].Text, func() {
|
|
r.appTabs.SelectIndex(index)
|
|
if r.appTabs.popUpMenu != nil {
|
|
r.appTabs.popUpMenu.Hide()
|
|
r.appTabs.popUpMenu = nil
|
|
}
|
|
}))
|
|
}
|
|
|
|
r.appTabs.popUpMenu = buildPopUpMenu(r.appTabs, overflow, items)
|
|
}}
|
|
|
|
return overflow
|
|
}
|
|
|
|
func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container {
|
|
buttons := &fyne.Container{}
|
|
|
|
var iconPos buttonIconPosition
|
|
if fyne.CurrentDevice().IsMobile() {
|
|
cells := count
|
|
if cells == 0 {
|
|
cells = 1
|
|
}
|
|
if r.appTabs.location == TabLocationTop || r.appTabs.location == TabLocationBottom {
|
|
buttons.Layout = layout.NewGridLayoutWithColumns(cells)
|
|
} else {
|
|
buttons.Layout = layout.NewGridLayoutWithRows(cells)
|
|
}
|
|
iconPos = buttonIconTop
|
|
} else if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
|
|
buttons.Layout = layout.NewVBoxLayout()
|
|
iconPos = buttonIconTop
|
|
} else {
|
|
buttons.Layout = layout.NewHBoxLayout()
|
|
iconPos = buttonIconInline
|
|
}
|
|
|
|
for i := 0; i < count; i++ {
|
|
item := r.appTabs.Items[i]
|
|
if item.button == nil {
|
|
item.button = &tabButton{
|
|
onTapped: func() { r.appTabs.Select(item) },
|
|
}
|
|
}
|
|
button := item.button
|
|
button.icon = item.Icon
|
|
button.iconPosition = iconPos
|
|
if i == r.appTabs.current {
|
|
button.importance = widget.HighImportance
|
|
} else {
|
|
button.importance = widget.MediumImportance
|
|
}
|
|
button.text = item.Text
|
|
button.textAlignment = fyne.TextAlignCenter
|
|
button.Refresh()
|
|
buttons.Objects = append(buttons.Objects, button)
|
|
}
|
|
return buttons
|
|
}
|
|
|
|
func (r *appTabsRenderer) updateIndicator(animate bool) {
|
|
if r.appTabs.current < 0 {
|
|
r.indicator.Hide()
|
|
return
|
|
}
|
|
|
|
var selectedPos fyne.Position
|
|
var selectedSize fyne.Size
|
|
|
|
buttons := r.bar.Objects[0].(*fyne.Container).Objects
|
|
if r.appTabs.current >= len(buttons) {
|
|
if a := r.action; a != nil {
|
|
selectedPos = a.Position()
|
|
selectedSize = a.Size()
|
|
}
|
|
} else {
|
|
selected := buttons[r.appTabs.current]
|
|
selectedPos = selected.Position()
|
|
selectedSize = selected.Size()
|
|
}
|
|
|
|
var indicatorPos fyne.Position
|
|
var indicatorSize fyne.Size
|
|
|
|
switch r.appTabs.location {
|
|
case TabLocationTop:
|
|
indicatorPos = fyne.NewPos(selectedPos.X, r.bar.MinSize().Height)
|
|
indicatorSize = fyne.NewSize(selectedSize.Width, theme.Padding())
|
|
case TabLocationLeading:
|
|
indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y)
|
|
indicatorSize = fyne.NewSize(theme.Padding(), selectedSize.Height)
|
|
case TabLocationBottom:
|
|
indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-theme.Padding())
|
|
indicatorSize = fyne.NewSize(selectedSize.Width, theme.Padding())
|
|
case TabLocationTrailing:
|
|
indicatorPos = fyne.NewPos(r.bar.Position().X-theme.Padding(), selectedPos.Y)
|
|
indicatorSize = fyne.NewSize(theme.Padding(), selectedSize.Height)
|
|
}
|
|
|
|
r.moveIndicator(indicatorPos, indicatorSize, animate)
|
|
}
|
|
|
|
func (r *appTabsRenderer) updateTabs(max int) {
|
|
tabCount := len(r.appTabs.Items)
|
|
|
|
// Set overflow action
|
|
if tabCount <= max {
|
|
r.action.Hide()
|
|
r.bar.Layout = layout.NewStackLayout()
|
|
} else {
|
|
tabCount = max
|
|
r.action.Show()
|
|
|
|
// Set layout of tab bar containing tab buttons and overflow action
|
|
if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
|
|
r.bar.Layout = layout.NewBorderLayout(nil, r.action, nil, nil)
|
|
} else {
|
|
r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.action)
|
|
}
|
|
}
|
|
|
|
buttons := r.buildTabButtons(tabCount)
|
|
|
|
r.bar.Objects = []fyne.CanvasObject{buttons}
|
|
if a := r.action; a != nil {
|
|
r.bar.Objects = append(r.bar.Objects, a)
|
|
}
|
|
|
|
r.bar.Refresh()
|
|
}
|