doctabs.go 12 KB

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