499 lines
12 KiB
Go
499 lines
12 KiB
Go
|
package container
|
||
|
|
||
|
import (
|
||
|
"image/color"
|
||
|
|
||
|
"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 = (*DocTabs)(nil)
|
||
|
|
||
|
// DocTabs container is used to display various pieces of content 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: 2.1
|
||
|
type DocTabs struct {
|
||
|
widget.BaseWidget
|
||
|
|
||
|
Items []*TabItem
|
||
|
|
||
|
CreateTab func() *TabItem
|
||
|
CloseIntercept func(*TabItem)
|
||
|
OnClosed func(*TabItem)
|
||
|
OnSelected func(*TabItem)
|
||
|
OnUnselected func(*TabItem)
|
||
|
|
||
|
current int
|
||
|
location TabLocation
|
||
|
isTransitioning bool
|
||
|
|
||
|
popUpMenu *widget.PopUpMenu
|
||
|
}
|
||
|
|
||
|
// NewDocTabs creates a new tab container that allows the user to choose between various pieces of content.
|
||
|
//
|
||
|
// Since: 2.1
|
||
|
func NewDocTabs(items ...*TabItem) *DocTabs {
|
||
|
tabs := &DocTabs{}
|
||
|
tabs.ExtendBaseWidget(tabs)
|
||
|
tabs.SetItems(items)
|
||
|
return tabs
|
||
|
}
|
||
|
|
||
|
// Append adds a new TabItem to the end of the tab bar.
|
||
|
func (t *DocTabs) Append(item *TabItem) {
|
||
|
t.SetItems(append(t.Items, item))
|
||
|
}
|
||
|
|
||
|
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
||
|
//
|
||
|
// Implements: fyne.Widget
|
||
|
func (t *DocTabs) CreateRenderer() fyne.WidgetRenderer {
|
||
|
t.ExtendBaseWidget(t)
|
||
|
r := &docTabsRenderer{
|
||
|
baseTabsRenderer: baseTabsRenderer{
|
||
|
bar: &fyne.Container{},
|
||
|
divider: canvas.NewRectangle(theme.ShadowColor()),
|
||
|
indicator: canvas.NewRectangle(theme.PrimaryColor()),
|
||
|
},
|
||
|
docTabs: t,
|
||
|
scroller: NewScroll(&fyne.Container{}),
|
||
|
}
|
||
|
r.action = r.buildAllTabsButton()
|
||
|
r.create = r.buildCreateTabsButton()
|
||
|
r.tabs = t
|
||
|
|
||
|
r.box = NewHBox(r.create, r.action)
|
||
|
r.scroller.OnScrolled = func(offset fyne.Position) {
|
||
|
r.updateIndicator(false)
|
||
|
}
|
||
|
r.updateAllTabs()
|
||
|
r.updateCreateTab()
|
||
|
r.updateTabs()
|
||
|
r.updateIndicator(false)
|
||
|
r.applyTheme(t)
|
||
|
return r
|
||
|
}
|
||
|
|
||
|
// DisableIndex disables the TabItem at the specified index.
|
||
|
//
|
||
|
// Since: 2.3
|
||
|
func (t *DocTabs) DisableIndex(i int) {
|
||
|
disableIndex(t, i)
|
||
|
}
|
||
|
|
||
|
// DisableItem disables the specified TabItem.
|
||
|
//
|
||
|
// Since: 2.3
|
||
|
func (t *DocTabs) DisableItem(item *TabItem) {
|
||
|
disableItem(t, item)
|
||
|
}
|
||
|
|
||
|
// EnableIndex enables the TabItem at the specified index.
|
||
|
//
|
||
|
// Since: 2.3
|
||
|
func (t *DocTabs) EnableIndex(i int) {
|
||
|
enableIndex(t, i)
|
||
|
}
|
||
|
|
||
|
// EnableItem enables the specified TabItem.
|
||
|
//
|
||
|
// Since: 2.3
|
||
|
func (t *DocTabs) EnableItem(item *TabItem) {
|
||
|
enableItem(t, item)
|
||
|
}
|
||
|
|
||
|
// Hide hides the widget.
|
||
|
//
|
||
|
// Implements: fyne.CanvasObject
|
||
|
func (t *DocTabs) 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 *DocTabs) MinSize() fyne.Size {
|
||
|
t.ExtendBaseWidget(t)
|
||
|
return t.BaseWidget.MinSize()
|
||
|
}
|
||
|
|
||
|
// Remove tab by value.
|
||
|
func (t *DocTabs) Remove(item *TabItem) {
|
||
|
removeItem(t, item)
|
||
|
t.Refresh()
|
||
|
}
|
||
|
|
||
|
// RemoveIndex removes tab by index.
|
||
|
func (t *DocTabs) RemoveIndex(index int) {
|
||
|
removeIndex(t, index)
|
||
|
t.Refresh()
|
||
|
}
|
||
|
|
||
|
// Select sets the specified TabItem to be selected and its content visible.
|
||
|
func (t *DocTabs) 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 *DocTabs) SelectIndex(index int) {
|
||
|
selectIndex(t, index)
|
||
|
t.Refresh()
|
||
|
}
|
||
|
|
||
|
// Selected returns the currently selected TabItem.
|
||
|
func (t *DocTabs) Selected() *TabItem {
|
||
|
return selected(t)
|
||
|
}
|
||
|
|
||
|
// SelectedIndex returns the index of the currently selected TabItem.
|
||
|
func (t *DocTabs) SelectedIndex() int {
|
||
|
return t.current
|
||
|
}
|
||
|
|
||
|
// SetItems sets the containers items and refreshes.
|
||
|
func (t *DocTabs) SetItems(items []*TabItem) {
|
||
|
setItems(t, items)
|
||
|
t.Refresh()
|
||
|
}
|
||
|
|
||
|
// SetTabLocation sets the location of the tab bar
|
||
|
func (t *DocTabs) SetTabLocation(l TabLocation) {
|
||
|
t.location = tabsAdjustedLocation(l)
|
||
|
t.Refresh()
|
||
|
}
|
||
|
|
||
|
// Show this widget, if it was previously hidden
|
||
|
//
|
||
|
// Implements: fyne.CanvasObject
|
||
|
func (t *DocTabs) Show() {
|
||
|
t.BaseWidget.Show()
|
||
|
t.SelectIndex(t.current)
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) close(item *TabItem) {
|
||
|
if f := t.CloseIntercept; f != nil {
|
||
|
f(item)
|
||
|
} else {
|
||
|
t.Remove(item)
|
||
|
if f := t.OnClosed; f != nil {
|
||
|
f(item)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) onUnselected() func(*TabItem) {
|
||
|
return t.OnUnselected
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) onSelected() func(*TabItem) {
|
||
|
return t.OnSelected
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) items() []*TabItem {
|
||
|
return t.Items
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) selected() int {
|
||
|
return t.current
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) setItems(items []*TabItem) {
|
||
|
t.Items = items
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) setSelected(selected int) {
|
||
|
t.current = selected
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) setTransitioning(transitioning bool) {
|
||
|
t.isTransitioning = transitioning
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) tabLocation() TabLocation {
|
||
|
return t.location
|
||
|
}
|
||
|
|
||
|
func (t *DocTabs) transitioning() bool {
|
||
|
return t.isTransitioning
|
||
|
}
|
||
|
|
||
|
// Declare conformity with WidgetRenderer interface.
|
||
|
var _ fyne.WidgetRenderer = (*docTabsRenderer)(nil)
|
||
|
|
||
|
type docTabsRenderer struct {
|
||
|
baseTabsRenderer
|
||
|
docTabs *DocTabs
|
||
|
scroller *Scroll
|
||
|
box *fyne.Container
|
||
|
create *widget.Button
|
||
|
lastSelected int
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) Layout(size fyne.Size) {
|
||
|
r.updateAllTabs()
|
||
|
r.updateCreateTab()
|
||
|
r.updateTabs()
|
||
|
r.layout(r.docTabs, size)
|
||
|
|
||
|
// lay out buttons before updating indicator, which is relative to their position
|
||
|
buttons := r.scroller.Content.(*fyne.Container)
|
||
|
buttons.Layout.Layout(buttons.Objects, buttons.Size())
|
||
|
r.updateIndicator(r.docTabs.transitioning())
|
||
|
|
||
|
if r.docTabs.transitioning() {
|
||
|
r.docTabs.setTransitioning(false)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) MinSize() fyne.Size {
|
||
|
return r.minSize(r.docTabs)
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) Objects() []fyne.CanvasObject {
|
||
|
return r.objects(r.docTabs)
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) Refresh() {
|
||
|
r.Layout(r.docTabs.Size())
|
||
|
|
||
|
if c := r.docTabs.current; c != r.lastSelected {
|
||
|
if c >= 0 && c < len(r.docTabs.Items) {
|
||
|
r.scrollToSelected()
|
||
|
}
|
||
|
r.lastSelected = c
|
||
|
}
|
||
|
|
||
|
r.refresh(r.docTabs)
|
||
|
|
||
|
canvas.Refresh(r.docTabs)
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) buildAllTabsButton() (all *widget.Button) {
|
||
|
all = &widget.Button{Importance: widget.LowImportance, OnTapped: func() {
|
||
|
// Show pop up containing all tabs
|
||
|
|
||
|
items := make([]*fyne.MenuItem, len(r.docTabs.Items))
|
||
|
for i := 0; i < len(r.docTabs.Items); i++ {
|
||
|
index := i // capture
|
||
|
// FIXME MenuItem doesn't support icons (#1752)
|
||
|
items[i] = fyne.NewMenuItem(r.docTabs.Items[i].Text, func() {
|
||
|
r.docTabs.SelectIndex(index)
|
||
|
if r.docTabs.popUpMenu != nil {
|
||
|
r.docTabs.popUpMenu.Hide()
|
||
|
r.docTabs.popUpMenu = nil
|
||
|
}
|
||
|
})
|
||
|
items[i].Checked = index == r.docTabs.current
|
||
|
}
|
||
|
|
||
|
r.docTabs.popUpMenu = buildPopUpMenu(r.docTabs, all, items)
|
||
|
}}
|
||
|
|
||
|
return all
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) buildCreateTabsButton() *widget.Button {
|
||
|
create := widget.NewButton("", func() {
|
||
|
if f := r.docTabs.CreateTab; f != nil {
|
||
|
if tab := f(); tab != nil {
|
||
|
r.docTabs.Append(tab)
|
||
|
r.docTabs.SelectIndex(len(r.docTabs.Items) - 1)
|
||
|
}
|
||
|
}
|
||
|
})
|
||
|
create.Importance = widget.LowImportance
|
||
|
return create
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) buildTabButtons(count int, buttons *fyne.Container) {
|
||
|
buttons.Objects = nil
|
||
|
|
||
|
var iconPos buttonIconPosition
|
||
|
if fyne.CurrentDevice().IsMobile() {
|
||
|
cells := count
|
||
|
if cells == 0 {
|
||
|
cells = 1
|
||
|
}
|
||
|
if r.docTabs.location == TabLocationTop || r.docTabs.location == TabLocationBottom {
|
||
|
buttons.Layout = layout.NewGridLayoutWithColumns(cells)
|
||
|
} else {
|
||
|
buttons.Layout = layout.NewGridLayoutWithRows(cells)
|
||
|
}
|
||
|
iconPos = buttonIconTop
|
||
|
} else if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
||
|
buttons.Layout = layout.NewVBoxLayout()
|
||
|
iconPos = buttonIconTop
|
||
|
} else {
|
||
|
buttons.Layout = layout.NewHBoxLayout()
|
||
|
iconPos = buttonIconInline
|
||
|
}
|
||
|
|
||
|
for i := 0; i < count; i++ {
|
||
|
item := r.docTabs.Items[i]
|
||
|
if item.button == nil {
|
||
|
item.button = &tabButton{
|
||
|
onTapped: func() { r.docTabs.Select(item) },
|
||
|
onClosed: func() { r.docTabs.close(item) },
|
||
|
}
|
||
|
}
|
||
|
button := item.button
|
||
|
button.icon = item.Icon
|
||
|
button.iconPosition = iconPos
|
||
|
if i == r.docTabs.current {
|
||
|
button.importance = widget.HighImportance
|
||
|
} else {
|
||
|
button.importance = widget.MediumImportance
|
||
|
}
|
||
|
button.text = item.Text
|
||
|
button.textAlignment = fyne.TextAlignLeading
|
||
|
button.Refresh()
|
||
|
buttons.Objects = append(buttons.Objects, button)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) scrollToSelected() {
|
||
|
buttons := r.scroller.Content.(*fyne.Container)
|
||
|
|
||
|
// https://github.com/fyne-io/fyne/issues/3909
|
||
|
// very dirty temporary fix to this crash!
|
||
|
if r.docTabs.current < 0 || r.docTabs.current >= len(buttons.Objects) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
button := buttons.Objects[r.docTabs.current]
|
||
|
pos := button.Position()
|
||
|
size := button.Size()
|
||
|
offset := r.scroller.Offset
|
||
|
viewport := r.scroller.Size()
|
||
|
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
||
|
if pos.Y < offset.Y {
|
||
|
offset.Y = pos.Y
|
||
|
} else if pos.Y+size.Height > offset.Y+viewport.Height {
|
||
|
offset.Y = pos.Y + size.Height - viewport.Height
|
||
|
}
|
||
|
} else {
|
||
|
if pos.X < offset.X {
|
||
|
offset.X = pos.X
|
||
|
} else if pos.X+size.Width > offset.X+viewport.Width {
|
||
|
offset.X = pos.X + size.Width - viewport.Width
|
||
|
}
|
||
|
}
|
||
|
r.scroller.Offset = offset
|
||
|
r.updateIndicator(false)
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) updateIndicator(animate bool) {
|
||
|
if r.docTabs.current < 0 {
|
||
|
r.indicator.FillColor = color.Transparent
|
||
|
r.moveIndicator(fyne.NewPos(0, 0), fyne.NewSize(0, 0), animate)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
var selectedPos fyne.Position
|
||
|
var selectedSize fyne.Size
|
||
|
|
||
|
buttons := r.scroller.Content.(*fyne.Container).Objects
|
||
|
|
||
|
if r.docTabs.current >= len(buttons) {
|
||
|
if a := r.action; a != nil {
|
||
|
selectedPos = a.Position()
|
||
|
selectedSize = a.Size()
|
||
|
minSize := a.MinSize()
|
||
|
if minSize.Width > selectedSize.Width {
|
||
|
selectedSize = minSize
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
selected := buttons[r.docTabs.current]
|
||
|
selectedPos = selected.Position()
|
||
|
selectedSize = selected.Size()
|
||
|
minSize := selected.MinSize()
|
||
|
if minSize.Width > selectedSize.Width {
|
||
|
selectedSize = minSize
|
||
|
}
|
||
|
}
|
||
|
|
||
|
scrollOffset := r.scroller.Offset
|
||
|
scrollSize := r.scroller.Size()
|
||
|
|
||
|
var indicatorPos fyne.Position
|
||
|
var indicatorSize fyne.Size
|
||
|
|
||
|
switch r.docTabs.location {
|
||
|
case TabLocationTop:
|
||
|
indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.MinSize().Height)
|
||
|
indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), theme.Padding())
|
||
|
case TabLocationLeading:
|
||
|
indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y-scrollOffset.Y)
|
||
|
indicatorSize = fyne.NewSize(theme.Padding(), fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
|
||
|
case TabLocationBottom:
|
||
|
indicatorPos = fyne.NewPos(selectedPos.X-scrollOffset.X, r.bar.Position().Y-theme.Padding())
|
||
|
indicatorSize = fyne.NewSize(fyne.Min(selectedSize.Width, scrollSize.Width-indicatorPos.X), theme.Padding())
|
||
|
case TabLocationTrailing:
|
||
|
indicatorPos = fyne.NewPos(r.bar.Position().X-theme.Padding(), selectedPos.Y-scrollOffset.Y)
|
||
|
indicatorSize = fyne.NewSize(theme.Padding(), fyne.Min(selectedSize.Height, scrollSize.Height-indicatorPos.Y))
|
||
|
}
|
||
|
|
||
|
if indicatorPos.X < 0 {
|
||
|
indicatorSize.Width = indicatorSize.Width + indicatorPos.X
|
||
|
indicatorPos.X = 0
|
||
|
}
|
||
|
if indicatorPos.Y < 0 {
|
||
|
indicatorSize.Height = indicatorSize.Height + indicatorPos.Y
|
||
|
indicatorPos.Y = 0
|
||
|
}
|
||
|
if indicatorSize.Width < 0 || indicatorSize.Height < 0 {
|
||
|
r.indicator.FillColor = color.Transparent
|
||
|
r.indicator.Refresh()
|
||
|
return
|
||
|
}
|
||
|
|
||
|
r.moveIndicator(indicatorPos, indicatorSize, animate)
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) updateAllTabs() {
|
||
|
if len(r.docTabs.Items) > 0 {
|
||
|
r.action.Show()
|
||
|
} else {
|
||
|
r.action.Hide()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) updateCreateTab() {
|
||
|
if r.docTabs.CreateTab != nil {
|
||
|
r.create.SetIcon(theme.ContentAddIcon())
|
||
|
r.create.Show()
|
||
|
} else {
|
||
|
r.create.Hide()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (r *docTabsRenderer) updateTabs() {
|
||
|
tabCount := len(r.docTabs.Items)
|
||
|
r.buildTabButtons(tabCount, r.scroller.Content.(*fyne.Container))
|
||
|
|
||
|
// Set layout of tab bar containing tab buttons and overflow action
|
||
|
if r.docTabs.location == TabLocationLeading || r.docTabs.location == TabLocationTrailing {
|
||
|
r.bar.Layout = layout.NewBorderLayout(nil, r.box, nil, nil)
|
||
|
r.scroller.Direction = ScrollVerticalOnly
|
||
|
} else {
|
||
|
r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.box)
|
||
|
r.scroller.Direction = ScrollHorizontalOnly
|
||
|
}
|
||
|
|
||
|
r.bar.Objects = []fyne.CanvasObject{r.scroller, r.box}
|
||
|
r.bar.Refresh()
|
||
|
}
|