| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- 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.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()
- }
|