apptabs.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. package container
  2. import (
  3. "fyne.io/fyne/v2"
  4. "fyne.io/fyne/v2/canvas"
  5. "fyne.io/fyne/v2/layout"
  6. "fyne.io/fyne/v2/theme"
  7. "fyne.io/fyne/v2/widget"
  8. )
  9. // Declare conformity with Widget interface.
  10. var _ fyne.Widget = (*AppTabs)(nil)
  11. // AppTabs container is used to split your application into various different areas identified by tabs.
  12. // The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
  13. // Each item is represented by a button at the edge of the container.
  14. //
  15. // Since: 1.4
  16. type AppTabs struct {
  17. widget.BaseWidget
  18. Items []*TabItem
  19. // Deprecated: Use `OnSelected func(*TabItem)` instead.
  20. OnChanged func(*TabItem)
  21. OnSelected func(*TabItem)
  22. OnUnselected func(*TabItem)
  23. current int
  24. location TabLocation
  25. isTransitioning bool
  26. popUpMenu *widget.PopUpMenu
  27. }
  28. // NewAppTabs creates a new tab container that allows the user to choose between different areas of an app.
  29. //
  30. // Since: 1.4
  31. func NewAppTabs(items ...*TabItem) *AppTabs {
  32. tabs := &AppTabs{}
  33. tabs.BaseWidget.ExtendBaseWidget(tabs)
  34. tabs.SetItems(items)
  35. return tabs
  36. }
  37. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  38. //
  39. // Implements: fyne.Widget
  40. func (t *AppTabs) CreateRenderer() fyne.WidgetRenderer {
  41. t.BaseWidget.ExtendBaseWidget(t)
  42. r := &appTabsRenderer{
  43. baseTabsRenderer: baseTabsRenderer{
  44. bar: &fyne.Container{},
  45. divider: canvas.NewRectangle(theme.ShadowColor()),
  46. indicator: canvas.NewRectangle(theme.PrimaryColor()),
  47. },
  48. appTabs: t,
  49. }
  50. r.action = r.buildOverflowTabsButton()
  51. // Initially setup the tab bar to only show one tab, all others will be in overflow.
  52. // When the widget is laid out, and we know the size, the tab bar will be updated to show as many as can fit.
  53. r.updateTabs(1)
  54. r.updateIndicator(false)
  55. r.applyTheme(t)
  56. return r
  57. }
  58. // Append adds a new TabItem to the end of the tab bar.
  59. func (t *AppTabs) Append(item *TabItem) {
  60. t.SetItems(append(t.Items, item))
  61. }
  62. // CurrentTab returns the currently selected TabItem.
  63. //
  64. // Deprecated: Use `AppTabs.Selected() *TabItem` instead.
  65. func (t *AppTabs) CurrentTab() *TabItem {
  66. if t.current < 0 || t.current >= len(t.Items) {
  67. return nil
  68. }
  69. return t.Items[t.current]
  70. }
  71. // CurrentTabIndex returns the index of the currently selected TabItem.
  72. //
  73. // Deprecated: Use `AppTabs.SelectedIndex() int` instead.
  74. func (t *AppTabs) CurrentTabIndex() int {
  75. return t.current
  76. }
  77. // DisableIndex disables the TabItem at the specified index.
  78. //
  79. // Since: 2.3
  80. func (t *AppTabs) DisableIndex(i int) {
  81. disableIndex(t, i)
  82. }
  83. // DisableItem disables the specified TabItem.
  84. //
  85. // Since: 2.3
  86. func (t *AppTabs) DisableItem(item *TabItem) {
  87. disableItem(t, item)
  88. }
  89. // EnableIndex enables the TabItem at the specified index.
  90. //
  91. // Since: 2.3
  92. func (t *AppTabs) EnableIndex(i int) {
  93. enableIndex(t, i)
  94. }
  95. // EnableItem enables the specified TabItem.
  96. //
  97. // Since: 2.3
  98. func (t *AppTabs) EnableItem(item *TabItem) {
  99. enableItem(t, item)
  100. }
  101. // ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
  102. //
  103. // Deprecated: Support for extending containers is being removed
  104. func (t *AppTabs) ExtendBaseWidget(wid fyne.Widget) {
  105. t.BaseWidget.ExtendBaseWidget(wid)
  106. }
  107. // Hide hides the widget.
  108. //
  109. // Implements: fyne.CanvasObject
  110. func (t *AppTabs) Hide() {
  111. if t.popUpMenu != nil {
  112. t.popUpMenu.Hide()
  113. t.popUpMenu = nil
  114. }
  115. t.BaseWidget.Hide()
  116. }
  117. // MinSize returns the size that this widget should not shrink below
  118. //
  119. // Implements: fyne.CanvasObject
  120. func (t *AppTabs) MinSize() fyne.Size {
  121. t.BaseWidget.ExtendBaseWidget(t)
  122. return t.BaseWidget.MinSize()
  123. }
  124. // Remove tab by value.
  125. func (t *AppTabs) Remove(item *TabItem) {
  126. removeItem(t, item)
  127. t.Refresh()
  128. }
  129. // RemoveIndex removes tab by index.
  130. func (t *AppTabs) RemoveIndex(index int) {
  131. removeIndex(t, index)
  132. t.Refresh()
  133. }
  134. // Select sets the specified TabItem to be selected and its content visible.
  135. func (t *AppTabs) Select(item *TabItem) {
  136. selectItem(t, item)
  137. t.Refresh()
  138. }
  139. // SelectIndex sets the TabItem at the specific index to be selected and its content visible.
  140. func (t *AppTabs) SelectIndex(index int) {
  141. selectIndex(t, index)
  142. t.Refresh()
  143. }
  144. // SelectTab sets the specified TabItem to be selected and its content visible.
  145. //
  146. // Deprecated: Use `AppTabs.Select(*TabItem)` instead.
  147. func (t *AppTabs) SelectTab(item *TabItem) {
  148. for i, child := range t.Items {
  149. if child == item {
  150. t.SelectTabIndex(i)
  151. return
  152. }
  153. }
  154. }
  155. // SelectTabIndex sets the TabItem at the specific index to be selected and its content visible.
  156. //
  157. // Deprecated: Use `AppTabs.SelectIndex(int)` instead.
  158. func (t *AppTabs) SelectTabIndex(index int) {
  159. if index < 0 || index >= len(t.Items) || t.current == index {
  160. return
  161. }
  162. t.current = index
  163. t.Refresh()
  164. if t.OnChanged != nil {
  165. t.OnChanged(t.Items[t.current])
  166. }
  167. }
  168. // Selected returns the currently selected TabItem.
  169. func (t *AppTabs) Selected() *TabItem {
  170. return selected(t)
  171. }
  172. // SelectedIndex returns the index of the currently selected TabItem.
  173. func (t *AppTabs) SelectedIndex() int {
  174. return t.current
  175. }
  176. // SetItems sets the containers items and refreshes.
  177. func (t *AppTabs) SetItems(items []*TabItem) {
  178. setItems(t, items)
  179. t.Refresh()
  180. }
  181. // SetTabLocation sets the location of the tab bar
  182. func (t *AppTabs) SetTabLocation(l TabLocation) {
  183. t.location = tabsAdjustedLocation(l)
  184. t.Refresh()
  185. }
  186. // Show this widget, if it was previously hidden
  187. //
  188. // Implements: fyne.CanvasObject
  189. func (t *AppTabs) Show() {
  190. t.BaseWidget.Show()
  191. t.SelectIndex(t.current)
  192. }
  193. func (t *AppTabs) onUnselected() func(*TabItem) {
  194. return t.OnUnselected
  195. }
  196. func (t *AppTabs) onSelected() func(*TabItem) {
  197. return func(tab *TabItem) {
  198. if f := t.OnChanged; f != nil {
  199. f(tab)
  200. }
  201. if f := t.OnSelected; f != nil {
  202. f(tab)
  203. }
  204. }
  205. }
  206. func (t *AppTabs) items() []*TabItem {
  207. return t.Items
  208. }
  209. func (t *AppTabs) selected() int {
  210. return t.current
  211. }
  212. func (t *AppTabs) setItems(items []*TabItem) {
  213. t.Items = items
  214. }
  215. func (t *AppTabs) setSelected(selected int) {
  216. t.current = selected
  217. }
  218. func (t *AppTabs) setTransitioning(transitioning bool) {
  219. t.isTransitioning = transitioning
  220. }
  221. func (t *AppTabs) tabLocation() TabLocation {
  222. return t.location
  223. }
  224. func (t *AppTabs) transitioning() bool {
  225. return t.isTransitioning
  226. }
  227. // Declare conformity with WidgetRenderer interface.
  228. var _ fyne.WidgetRenderer = (*appTabsRenderer)(nil)
  229. type appTabsRenderer struct {
  230. baseTabsRenderer
  231. appTabs *AppTabs
  232. }
  233. func (r *appTabsRenderer) Layout(size fyne.Size) {
  234. // Try render as many tabs as will fit, others will appear in the overflow
  235. if len(r.appTabs.Items) == 0 {
  236. r.updateTabs(0)
  237. } else {
  238. for i := len(r.appTabs.Items); i > 0; i-- {
  239. r.updateTabs(i)
  240. barMin := r.bar.MinSize()
  241. if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
  242. if barMin.Height <= size.Height {
  243. // Tab bar is short enough to fit
  244. break
  245. }
  246. } else {
  247. if barMin.Width <= size.Width {
  248. // Tab bar is thin enough to fit
  249. break
  250. }
  251. }
  252. }
  253. }
  254. r.layout(r.appTabs, size)
  255. r.updateIndicator(r.appTabs.transitioning())
  256. if r.appTabs.transitioning() {
  257. r.appTabs.setTransitioning(false)
  258. }
  259. }
  260. func (r *appTabsRenderer) MinSize() fyne.Size {
  261. return r.minSize(r.appTabs)
  262. }
  263. func (r *appTabsRenderer) Objects() []fyne.CanvasObject {
  264. return r.objects(r.appTabs)
  265. }
  266. func (r *appTabsRenderer) Refresh() {
  267. r.Layout(r.appTabs.Size())
  268. r.refresh(r.appTabs)
  269. canvas.Refresh(r.appTabs)
  270. }
  271. func (r *appTabsRenderer) buildOverflowTabsButton() (overflow *widget.Button) {
  272. overflow = &widget.Button{Icon: moreIcon(r.appTabs), Importance: widget.LowImportance, OnTapped: func() {
  273. // Show pop up containing all tabs which did not fit in the tab bar
  274. itemLen, objLen := len(r.appTabs.Items), len(r.bar.Objects[0].(*fyne.Container).Objects)
  275. items := make([]*fyne.MenuItem, 0, itemLen-objLen)
  276. for i := objLen; i < itemLen; i++ {
  277. index := i // capture
  278. // FIXME MenuItem doesn't support icons (#1752)
  279. // FIXME MenuItem can't show if it is the currently selected tab (#1753)
  280. items = append(items, fyne.NewMenuItem(r.appTabs.Items[i].Text, func() {
  281. r.appTabs.SelectIndex(index)
  282. if r.appTabs.popUpMenu != nil {
  283. r.appTabs.popUpMenu.Hide()
  284. r.appTabs.popUpMenu = nil
  285. }
  286. }))
  287. }
  288. r.appTabs.popUpMenu = buildPopUpMenu(r.appTabs, overflow, items)
  289. }}
  290. return overflow
  291. }
  292. func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container {
  293. buttons := &fyne.Container{}
  294. var iconPos buttonIconPosition
  295. if fyne.CurrentDevice().IsMobile() {
  296. cells := count
  297. if cells == 0 {
  298. cells = 1
  299. }
  300. if r.appTabs.location == TabLocationTop || r.appTabs.location == TabLocationBottom {
  301. buttons.Layout = layout.NewGridLayoutWithColumns(cells)
  302. } else {
  303. buttons.Layout = layout.NewGridLayoutWithRows(cells)
  304. }
  305. iconPos = buttonIconTop
  306. } else if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
  307. buttons.Layout = layout.NewVBoxLayout()
  308. iconPos = buttonIconTop
  309. } else {
  310. buttons.Layout = layout.NewHBoxLayout()
  311. iconPos = buttonIconInline
  312. }
  313. for i := 0; i < count; i++ {
  314. item := r.appTabs.Items[i]
  315. if item.button == nil {
  316. item.button = &tabButton{
  317. onTapped: func() { r.appTabs.Select(item) },
  318. }
  319. }
  320. button := item.button
  321. button.icon = item.Icon
  322. button.iconPosition = iconPos
  323. if i == r.appTabs.current {
  324. button.importance = widget.HighImportance
  325. } else {
  326. button.importance = widget.MediumImportance
  327. }
  328. button.text = item.Text
  329. button.textAlignment = fyne.TextAlignCenter
  330. button.Refresh()
  331. buttons.Objects = append(buttons.Objects, button)
  332. }
  333. return buttons
  334. }
  335. func (r *appTabsRenderer) updateIndicator(animate bool) {
  336. if r.appTabs.current < 0 {
  337. r.indicator.Hide()
  338. return
  339. }
  340. var selectedPos fyne.Position
  341. var selectedSize fyne.Size
  342. buttons := r.bar.Objects[0].(*fyne.Container).Objects
  343. if r.appTabs.current >= len(buttons) {
  344. if a := r.action; a != nil {
  345. selectedPos = a.Position()
  346. selectedSize = a.Size()
  347. }
  348. } else {
  349. selected := buttons[r.appTabs.current]
  350. selectedPos = selected.Position()
  351. selectedSize = selected.Size()
  352. }
  353. var indicatorPos fyne.Position
  354. var indicatorSize fyne.Size
  355. switch r.appTabs.location {
  356. case TabLocationTop:
  357. indicatorPos = fyne.NewPos(selectedPos.X, r.bar.MinSize().Height)
  358. indicatorSize = fyne.NewSize(selectedSize.Width, theme.Padding())
  359. case TabLocationLeading:
  360. indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y)
  361. indicatorSize = fyne.NewSize(theme.Padding(), selectedSize.Height)
  362. case TabLocationBottom:
  363. indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-theme.Padding())
  364. indicatorSize = fyne.NewSize(selectedSize.Width, theme.Padding())
  365. case TabLocationTrailing:
  366. indicatorPos = fyne.NewPos(r.bar.Position().X-theme.Padding(), selectedPos.Y)
  367. indicatorSize = fyne.NewSize(theme.Padding(), selectedSize.Height)
  368. }
  369. r.moveIndicator(indicatorPos, indicatorSize, animate)
  370. }
  371. func (r *appTabsRenderer) updateTabs(max int) {
  372. tabCount := len(r.appTabs.Items)
  373. // Set overflow action
  374. if tabCount <= max {
  375. r.action.Hide()
  376. r.bar.Layout = layout.NewStackLayout()
  377. } else {
  378. tabCount = max
  379. r.action.Show()
  380. // Set layout of tab bar containing tab buttons and overflow action
  381. if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
  382. r.bar.Layout = layout.NewBorderLayout(nil, r.action, nil, nil)
  383. } else {
  384. r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.action)
  385. }
  386. }
  387. buttons := r.buildTabButtons(tabCount)
  388. r.bar.Objects = []fyne.CanvasObject{buttons}
  389. if a := r.action; a != nil {
  390. r.bar.Objects = append(r.bar.Objects, a)
  391. }
  392. r.bar.Refresh()
  393. }