tabs.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832
  1. package container
  2. import (
  3. "sync"
  4. "fyne.io/fyne/v2"
  5. "fyne.io/fyne/v2/canvas"
  6. "fyne.io/fyne/v2/driver/desktop"
  7. "fyne.io/fyne/v2/internal"
  8. "fyne.io/fyne/v2/theme"
  9. "fyne.io/fyne/v2/widget"
  10. )
  11. // TabItem represents a single view in a tab view.
  12. // The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active.
  13. //
  14. // Since: 1.4
  15. type TabItem struct {
  16. Text string
  17. Icon fyne.Resource
  18. Content fyne.CanvasObject
  19. button *tabButton
  20. }
  21. // Disabled returns whether or not the TabItem is disabled.
  22. //
  23. // Since: 2.3
  24. func (ti *TabItem) Disabled() bool {
  25. if ti.button != nil {
  26. return ti.button.Disabled()
  27. }
  28. return false
  29. }
  30. func (ti *TabItem) disable() {
  31. if ti.button != nil {
  32. ti.button.Disable()
  33. }
  34. }
  35. func (ti *TabItem) enable() {
  36. if ti.button != nil {
  37. ti.button.Enable()
  38. }
  39. }
  40. // TabLocation is the location where the tabs of a tab container should be rendered
  41. //
  42. // Since: 1.4
  43. type TabLocation int
  44. // TabLocation values
  45. const (
  46. TabLocationTop TabLocation = iota
  47. TabLocationLeading
  48. TabLocationBottom
  49. TabLocationTrailing
  50. )
  51. // NewTabItem creates a new item for a tabbed widget - each item specifies the content and a label for its tab.
  52. //
  53. // Since: 1.4
  54. func NewTabItem(text string, content fyne.CanvasObject) *TabItem {
  55. return &TabItem{Text: text, Content: content}
  56. }
  57. // NewTabItemWithIcon creates a new item for a tabbed widget - each item specifies the content and a label with an icon for its tab.
  58. //
  59. // Since: 1.4
  60. func NewTabItemWithIcon(text string, icon fyne.Resource, content fyne.CanvasObject) *TabItem {
  61. return &TabItem{Text: text, Icon: icon, Content: content}
  62. }
  63. type baseTabs interface {
  64. onUnselected() func(*TabItem)
  65. onSelected() func(*TabItem)
  66. items() []*TabItem
  67. setItems([]*TabItem)
  68. selected() int
  69. setSelected(int)
  70. tabLocation() TabLocation
  71. transitioning() bool
  72. setTransitioning(bool)
  73. }
  74. func tabsAdjustedLocation(l TabLocation) TabLocation {
  75. // Mobile has limited screen space, so don't put app tab bar on long edges
  76. if d := fyne.CurrentDevice(); d.IsMobile() {
  77. if o := d.Orientation(); fyne.IsVertical(o) {
  78. if l == TabLocationLeading {
  79. return TabLocationTop
  80. } else if l == TabLocationTrailing {
  81. return TabLocationBottom
  82. }
  83. } else {
  84. if l == TabLocationTop {
  85. return TabLocationLeading
  86. } else if l == TabLocationBottom {
  87. return TabLocationTrailing
  88. }
  89. }
  90. }
  91. return l
  92. }
  93. func buildPopUpMenu(t baseTabs, button *widget.Button, items []*fyne.MenuItem) *widget.PopUpMenu {
  94. d := fyne.CurrentApp().Driver()
  95. c := d.CanvasForObject(button)
  96. popUpMenu := widget.NewPopUpMenu(fyne.NewMenu("", items...), c)
  97. buttonPos := d.AbsolutePositionForObject(button)
  98. buttonSize := button.Size()
  99. popUpMin := popUpMenu.MinSize()
  100. var popUpPos fyne.Position
  101. switch t.tabLocation() {
  102. case TabLocationLeading:
  103. popUpPos.X = buttonPos.X + buttonSize.Width
  104. popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
  105. case TabLocationTrailing:
  106. popUpPos.X = buttonPos.X - popUpMin.Width
  107. popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
  108. case TabLocationTop:
  109. popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
  110. popUpPos.Y = buttonPos.Y + buttonSize.Height
  111. case TabLocationBottom:
  112. popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
  113. popUpPos.Y = buttonPos.Y - popUpMin.Height
  114. }
  115. if popUpPos.X < 0 {
  116. popUpPos.X = 0
  117. }
  118. if popUpPos.Y < 0 {
  119. popUpPos.Y = 0
  120. }
  121. popUpMenu.ShowAtPosition(popUpPos)
  122. return popUpMenu
  123. }
  124. func removeIndex(t baseTabs, index int) {
  125. items := t.items()
  126. if index < 0 || index >= len(items) {
  127. return
  128. }
  129. setItems(t, append(items[:index], items[index+1:]...))
  130. if s := t.selected(); index < s {
  131. t.setSelected(s - 1)
  132. }
  133. }
  134. func removeItem(t baseTabs, item *TabItem) {
  135. for index, existingItem := range t.items() {
  136. if existingItem == item {
  137. removeIndex(t, index)
  138. break
  139. }
  140. }
  141. }
  142. func selected(t baseTabs) *TabItem {
  143. selected := t.selected()
  144. items := t.items()
  145. if selected < 0 || selected >= len(items) {
  146. return nil
  147. }
  148. return items[selected]
  149. }
  150. func selectIndex(t baseTabs, index int) {
  151. selected := t.selected()
  152. if selected == index {
  153. // No change, so do nothing
  154. return
  155. }
  156. items := t.items()
  157. if f := t.onUnselected(); f != nil && selected >= 0 && selected < len(items) {
  158. // Notification of unselected
  159. f(items[selected])
  160. }
  161. if index < 0 || index >= len(items) {
  162. // Out of bounds, so do nothing
  163. return
  164. }
  165. t.setTransitioning(true)
  166. t.setSelected(index)
  167. if f := t.onSelected(); f != nil {
  168. // Notification of selected
  169. f(items[index])
  170. }
  171. }
  172. func selectItem(t baseTabs, item *TabItem) {
  173. for i, child := range t.items() {
  174. if child == item {
  175. selectIndex(t, i)
  176. return
  177. }
  178. }
  179. }
  180. func setItems(t baseTabs, items []*TabItem) {
  181. if mismatchedTabItems(items) {
  182. internal.LogHint("Tab items should all have the same type of content (text, icons or both)")
  183. }
  184. t.setItems(items)
  185. selected := t.selected()
  186. count := len(items)
  187. switch {
  188. case count == 0:
  189. // No items available to be selected
  190. selectIndex(t, -1) // Unsure OnUnselected gets called if applicable
  191. t.setSelected(-1)
  192. case selected < 0:
  193. // Current is first tab item
  194. selectIndex(t, 0)
  195. case selected >= count:
  196. // Current doesn't exist, select last tab
  197. selectIndex(t, count-1)
  198. }
  199. }
  200. func disableIndex(t baseTabs, index int) {
  201. items := t.items()
  202. if index < 0 || index >= len(items) {
  203. return
  204. }
  205. item := items[index]
  206. item.disable()
  207. if selected(t) == item {
  208. // the disabled tab is currently selected, so select the first enabled tab
  209. for i, it := range items {
  210. if !it.Disabled() {
  211. selectIndex(t, i)
  212. break
  213. }
  214. }
  215. }
  216. if selected(t) == item {
  217. selectIndex(t, -1) // no other tab is able to be selected
  218. }
  219. }
  220. func disableItem(t baseTabs, item *TabItem) {
  221. for i, it := range t.items() {
  222. if it == item {
  223. disableIndex(t, i)
  224. return
  225. }
  226. }
  227. }
  228. func enableIndex(t baseTabs, index int) {
  229. items := t.items()
  230. if index < 0 || index >= len(items) {
  231. return
  232. }
  233. item := items[index]
  234. item.enable()
  235. }
  236. func enableItem(t baseTabs, item *TabItem) {
  237. for i, it := range t.items() {
  238. if it == item {
  239. enableIndex(t, i)
  240. return
  241. }
  242. }
  243. }
  244. type baseTabsRenderer struct {
  245. positionAnimation, sizeAnimation *fyne.Animation
  246. lastIndicatorMutex sync.RWMutex
  247. lastIndicatorPos fyne.Position
  248. lastIndicatorSize fyne.Size
  249. lastIndicatorHidden bool
  250. action *widget.Button
  251. bar *fyne.Container
  252. divider, indicator *canvas.Rectangle
  253. }
  254. func (r *baseTabsRenderer) Destroy() {
  255. }
  256. func (r *baseTabsRenderer) applyTheme(t baseTabs) {
  257. if r.action != nil {
  258. r.action.SetIcon(moreIcon(t))
  259. }
  260. r.divider.FillColor = theme.ShadowColor()
  261. r.indicator.FillColor = theme.PrimaryColor()
  262. }
  263. func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
  264. var (
  265. barPos, dividerPos, contentPos fyne.Position
  266. barSize, dividerSize, contentSize fyne.Size
  267. )
  268. barMin := r.bar.MinSize()
  269. switch t.tabLocation() {
  270. case TabLocationTop:
  271. barHeight := barMin.Height
  272. barPos = fyne.NewPos(0, 0)
  273. barSize = fyne.NewSize(size.Width, barHeight)
  274. dividerPos = fyne.NewPos(0, barHeight)
  275. dividerSize = fyne.NewSize(size.Width, theme.Padding())
  276. contentPos = fyne.NewPos(0, barHeight+theme.Padding())
  277. contentSize = fyne.NewSize(size.Width, size.Height-barHeight-theme.Padding())
  278. case TabLocationLeading:
  279. barWidth := barMin.Width
  280. barPos = fyne.NewPos(0, 0)
  281. barSize = fyne.NewSize(barWidth, size.Height)
  282. dividerPos = fyne.NewPos(barWidth, 0)
  283. dividerSize = fyne.NewSize(theme.Padding(), size.Height)
  284. contentPos = fyne.NewPos(barWidth+theme.Padding(), 0)
  285. contentSize = fyne.NewSize(size.Width-barWidth-theme.Padding(), size.Height)
  286. case TabLocationBottom:
  287. barHeight := barMin.Height
  288. barPos = fyne.NewPos(0, size.Height-barHeight)
  289. barSize = fyne.NewSize(size.Width, barHeight)
  290. dividerPos = fyne.NewPos(0, size.Height-barHeight-theme.Padding())
  291. dividerSize = fyne.NewSize(size.Width, theme.Padding())
  292. contentPos = fyne.NewPos(0, 0)
  293. contentSize = fyne.NewSize(size.Width, size.Height-barHeight-theme.Padding())
  294. case TabLocationTrailing:
  295. barWidth := barMin.Width
  296. barPos = fyne.NewPos(size.Width-barWidth, 0)
  297. barSize = fyne.NewSize(barWidth, size.Height)
  298. dividerPos = fyne.NewPos(size.Width-barWidth-theme.Padding(), 0)
  299. dividerSize = fyne.NewSize(theme.Padding(), size.Height)
  300. contentPos = fyne.NewPos(0, 0)
  301. contentSize = fyne.NewSize(size.Width-barWidth-theme.Padding(), size.Height)
  302. }
  303. r.bar.Move(barPos)
  304. r.bar.Resize(barSize)
  305. r.divider.Move(dividerPos)
  306. r.divider.Resize(dividerSize)
  307. selected := t.selected()
  308. for i, ti := range t.items() {
  309. if i == selected {
  310. ti.Content.Move(contentPos)
  311. ti.Content.Resize(contentSize)
  312. ti.Content.Show()
  313. } else {
  314. ti.Content.Hide()
  315. }
  316. }
  317. }
  318. func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size {
  319. pad := theme.Padding()
  320. buttonPad := pad
  321. barMin := r.bar.MinSize()
  322. tabsMin := r.bar.Objects[0].MinSize()
  323. accessory := r.bar.Objects[1]
  324. accessoryMin := accessory.MinSize()
  325. if scroll, ok := r.bar.Objects[0].(*Scroll); ok && len(scroll.Content.(*fyne.Container).Objects) == 0 {
  326. tabsMin = fyne.Size{} // scroller forces 32 where we don't need any space
  327. buttonPad = 0
  328. } else if group, ok := r.bar.Objects[0].(*fyne.Container); ok && len(group.Objects) > 0 {
  329. tabsMin = group.Objects[0].MinSize()
  330. buttonPad = 0
  331. }
  332. if !accessory.Visible() || accessoryMin.Width == 0 {
  333. buttonPad = 0
  334. accessoryMin = fyne.Size{}
  335. }
  336. contentMin := fyne.NewSize(0, 0)
  337. for _, content := range t.items() {
  338. contentMin = contentMin.Max(content.Content.MinSize())
  339. }
  340. switch t.tabLocation() {
  341. case TabLocationLeading, TabLocationTrailing:
  342. return fyne.NewSize(barMin.Width+contentMin.Width+pad,
  343. fyne.Max(contentMin.Height, accessoryMin.Height+buttonPad+tabsMin.Height))
  344. default:
  345. return fyne.NewSize(fyne.Max(contentMin.Width, accessoryMin.Width+buttonPad+tabsMin.Width),
  346. barMin.Height+contentMin.Height+pad)
  347. }
  348. }
  349. func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, animate bool) {
  350. r.lastIndicatorMutex.RLock()
  351. isSameState := r.lastIndicatorPos.Subtract(pos).IsZero() && r.lastIndicatorSize.Subtract(siz).IsZero() &&
  352. r.lastIndicatorHidden == r.indicator.Hidden
  353. r.lastIndicatorMutex.RUnlock()
  354. if isSameState {
  355. return
  356. }
  357. if r.positionAnimation != nil {
  358. r.positionAnimation.Stop()
  359. r.positionAnimation = nil
  360. }
  361. if r.sizeAnimation != nil {
  362. r.sizeAnimation.Stop()
  363. r.sizeAnimation = nil
  364. }
  365. r.indicator.FillColor = theme.PrimaryColor()
  366. if r.indicator.Position().IsZero() {
  367. r.indicator.Move(pos)
  368. r.indicator.Resize(siz)
  369. r.indicator.Refresh()
  370. return
  371. }
  372. r.lastIndicatorMutex.Lock()
  373. r.lastIndicatorPos = pos
  374. r.lastIndicatorSize = siz
  375. r.lastIndicatorHidden = r.indicator.Hidden
  376. r.lastIndicatorMutex.Unlock()
  377. if animate {
  378. r.positionAnimation = canvas.NewPositionAnimation(r.indicator.Position(), pos, canvas.DurationShort, func(p fyne.Position) {
  379. r.indicator.Move(p)
  380. r.indicator.Refresh()
  381. if pos == p {
  382. r.positionAnimation.Stop()
  383. r.positionAnimation = nil
  384. }
  385. })
  386. r.sizeAnimation = canvas.NewSizeAnimation(r.indicator.Size(), siz, canvas.DurationShort, func(s fyne.Size) {
  387. r.indicator.Resize(s)
  388. r.indicator.Refresh()
  389. if siz == s {
  390. r.sizeAnimation.Stop()
  391. r.sizeAnimation = nil
  392. }
  393. })
  394. r.positionAnimation.Start()
  395. r.sizeAnimation.Start()
  396. } else {
  397. r.indicator.Move(pos)
  398. r.indicator.Resize(siz)
  399. r.indicator.Refresh()
  400. }
  401. }
  402. func (r *baseTabsRenderer) objects(t baseTabs) []fyne.CanvasObject {
  403. objects := []fyne.CanvasObject{r.bar, r.divider, r.indicator}
  404. if i, is := t.selected(), t.items(); i >= 0 && i < len(is) {
  405. objects = append(objects, is[i].Content)
  406. }
  407. return objects
  408. }
  409. func (r *baseTabsRenderer) refresh(t baseTabs) {
  410. r.applyTheme(t)
  411. r.bar.Refresh()
  412. r.divider.Refresh()
  413. r.indicator.Refresh()
  414. }
  415. type buttonIconPosition int
  416. const (
  417. buttonIconInline buttonIconPosition = iota
  418. buttonIconTop
  419. )
  420. var _ fyne.Widget = (*tabButton)(nil)
  421. var _ fyne.Tappable = (*tabButton)(nil)
  422. var _ desktop.Hoverable = (*tabButton)(nil)
  423. type tabButton struct {
  424. widget.DisableableWidget
  425. hovered bool
  426. icon fyne.Resource
  427. iconPosition buttonIconPosition
  428. importance widget.ButtonImportance
  429. onTapped func()
  430. onClosed func()
  431. text string
  432. textAlignment fyne.TextAlign
  433. }
  434. func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
  435. b.ExtendBaseWidget(b)
  436. background := canvas.NewRectangle(theme.HoverColor())
  437. background.Hide()
  438. icon := canvas.NewImageFromResource(b.icon)
  439. if b.icon == nil {
  440. icon.Hide()
  441. }
  442. label := canvas.NewText(b.text, theme.ForegroundColor())
  443. label.TextStyle.Bold = true
  444. close := &tabCloseButton{
  445. parent: b,
  446. onTapped: func() {
  447. if f := b.onClosed; f != nil {
  448. f()
  449. }
  450. },
  451. }
  452. close.ExtendBaseWidget(close)
  453. close.Hide()
  454. objects := []fyne.CanvasObject{background, label, close, icon}
  455. r := &tabButtonRenderer{
  456. button: b,
  457. background: background,
  458. icon: icon,
  459. label: label,
  460. close: close,
  461. objects: objects,
  462. }
  463. r.Refresh()
  464. return r
  465. }
  466. func (b *tabButton) MinSize() fyne.Size {
  467. b.ExtendBaseWidget(b)
  468. return b.BaseWidget.MinSize()
  469. }
  470. func (b *tabButton) MouseIn(*desktop.MouseEvent) {
  471. b.hovered = true
  472. b.Refresh()
  473. }
  474. func (b *tabButton) MouseMoved(*desktop.MouseEvent) {
  475. }
  476. func (b *tabButton) MouseOut() {
  477. b.hovered = false
  478. b.Refresh()
  479. }
  480. func (b *tabButton) Tapped(*fyne.PointEvent) {
  481. if b.Disabled() {
  482. return
  483. }
  484. b.onTapped()
  485. }
  486. type tabButtonRenderer struct {
  487. button *tabButton
  488. background *canvas.Rectangle
  489. icon *canvas.Image
  490. label *canvas.Text
  491. close *tabCloseButton
  492. objects []fyne.CanvasObject
  493. }
  494. func (r *tabButtonRenderer) Destroy() {
  495. }
  496. func (r *tabButtonRenderer) Layout(size fyne.Size) {
  497. r.background.Resize(size)
  498. padding := r.padding()
  499. innerSize := size.Subtract(padding)
  500. innerOffset := fyne.NewPos(padding.Width/2, padding.Height/2)
  501. labelShift := float32(0)
  502. if r.icon.Visible() {
  503. var iconOffset fyne.Position
  504. if r.button.iconPosition == buttonIconTop {
  505. iconOffset = fyne.NewPos((innerSize.Width-r.iconSize())/2, 0)
  506. } else {
  507. iconOffset = fyne.NewPos(0, (innerSize.Height-r.iconSize())/2)
  508. }
  509. r.icon.Resize(fyne.NewSize(r.iconSize(), r.iconSize()))
  510. r.icon.Move(innerOffset.Add(iconOffset))
  511. labelShift = r.iconSize() + theme.Padding()
  512. }
  513. if r.label.Text != "" {
  514. var labelOffset fyne.Position
  515. var labelSize fyne.Size
  516. if r.button.iconPosition == buttonIconTop {
  517. labelOffset = fyne.NewPos(0, labelShift)
  518. labelSize = fyne.NewSize(innerSize.Width, r.label.MinSize().Height)
  519. } else {
  520. labelOffset = fyne.NewPos(labelShift, 0)
  521. labelSize = fyne.NewSize(innerSize.Width-labelShift, innerSize.Height)
  522. }
  523. r.label.Resize(labelSize)
  524. r.label.Move(innerOffset.Add(labelOffset))
  525. }
  526. r.close.Move(fyne.NewPos(size.Width-theme.IconInlineSize()-theme.Padding(), (size.Height-theme.IconInlineSize())/2))
  527. r.close.Resize(fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize()))
  528. }
  529. func (r *tabButtonRenderer) MinSize() fyne.Size {
  530. var contentWidth, contentHeight float32
  531. textSize := r.label.MinSize()
  532. if r.button.iconPosition == buttonIconTop {
  533. contentWidth = fyne.Max(textSize.Width, r.iconSize())
  534. if r.icon.Visible() {
  535. contentHeight += r.iconSize()
  536. }
  537. if r.label.Text != "" {
  538. if r.icon.Visible() {
  539. contentHeight += theme.Padding()
  540. }
  541. contentHeight += textSize.Height
  542. }
  543. } else {
  544. contentHeight = fyne.Max(textSize.Height, r.iconSize())
  545. if r.icon.Visible() {
  546. contentWidth += r.iconSize()
  547. }
  548. if r.label.Text != "" {
  549. if r.icon.Visible() {
  550. contentWidth += theme.Padding()
  551. }
  552. contentWidth += textSize.Width
  553. }
  554. }
  555. if r.button.onClosed != nil {
  556. contentWidth += theme.IconInlineSize() + theme.Padding()
  557. contentHeight = fyne.Max(contentHeight, theme.IconInlineSize())
  558. }
  559. return fyne.NewSize(contentWidth, contentHeight).Add(r.padding())
  560. }
  561. func (r *tabButtonRenderer) Objects() []fyne.CanvasObject {
  562. return r.objects
  563. }
  564. func (r *tabButtonRenderer) Refresh() {
  565. if r.button.hovered && !r.button.Disabled() {
  566. r.background.FillColor = theme.HoverColor()
  567. r.background.Show()
  568. } else {
  569. r.background.Hide()
  570. }
  571. r.background.Refresh()
  572. r.label.Text = r.button.text
  573. r.label.Alignment = r.button.textAlignment
  574. if !r.button.Disabled() {
  575. if r.button.importance == widget.HighImportance {
  576. r.label.Color = theme.PrimaryColor()
  577. } else {
  578. r.label.Color = theme.ForegroundColor()
  579. }
  580. } else {
  581. r.label.Color = theme.DisabledTextColor()
  582. }
  583. r.label.TextSize = theme.TextSize()
  584. if r.button.text == "" {
  585. r.label.Hide()
  586. } else {
  587. r.label.Show()
  588. }
  589. r.icon.Resource = r.button.icon
  590. if r.icon.Resource != nil {
  591. r.icon.Show()
  592. switch res := r.icon.Resource.(type) {
  593. case *theme.ThemedResource:
  594. if r.button.importance == widget.HighImportance {
  595. r.icon.Resource = theme.NewPrimaryThemedResource(res)
  596. r.icon.Refresh()
  597. }
  598. case *theme.PrimaryThemedResource:
  599. if r.button.importance != widget.HighImportance {
  600. r.icon.Resource = res.Original()
  601. r.icon.Refresh()
  602. }
  603. }
  604. } else {
  605. r.icon.Hide()
  606. }
  607. if d := fyne.CurrentDevice(); r.button.onClosed != nil && (d.IsMobile() || r.button.hovered || r.close.hovered) {
  608. r.close.Show()
  609. } else {
  610. r.close.Hide()
  611. }
  612. r.close.Refresh()
  613. canvas.Refresh(r.button)
  614. }
  615. func (r *tabButtonRenderer) iconSize() float32 {
  616. switch r.button.iconPosition {
  617. case buttonIconTop:
  618. return 2 * theme.IconInlineSize()
  619. default:
  620. return theme.IconInlineSize()
  621. }
  622. }
  623. func (r *tabButtonRenderer) padding() fyne.Size {
  624. if r.label.Text != "" && r.button.iconPosition == buttonIconInline {
  625. return fyne.NewSize(theme.InnerPadding()*2, theme.InnerPadding()*2)
  626. }
  627. return fyne.NewSize(theme.InnerPadding(), theme.InnerPadding()*2)
  628. }
  629. var _ fyne.Widget = (*tabCloseButton)(nil)
  630. var _ fyne.Tappable = (*tabCloseButton)(nil)
  631. var _ desktop.Hoverable = (*tabCloseButton)(nil)
  632. type tabCloseButton struct {
  633. widget.BaseWidget
  634. parent *tabButton
  635. hovered bool
  636. onTapped func()
  637. }
  638. func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer {
  639. b.ExtendBaseWidget(b)
  640. background := canvas.NewRectangle(theme.HoverColor())
  641. background.Hide()
  642. icon := canvas.NewImageFromResource(theme.CancelIcon())
  643. r := &tabCloseButtonRenderer{
  644. button: b,
  645. background: background,
  646. icon: icon,
  647. objects: []fyne.CanvasObject{background, icon},
  648. }
  649. r.Refresh()
  650. return r
  651. }
  652. func (b *tabCloseButton) MinSize() fyne.Size {
  653. b.ExtendBaseWidget(b)
  654. return b.BaseWidget.MinSize()
  655. }
  656. func (b *tabCloseButton) MouseIn(*desktop.MouseEvent) {
  657. b.hovered = true
  658. b.parent.Refresh()
  659. }
  660. func (b *tabCloseButton) MouseMoved(*desktop.MouseEvent) {
  661. }
  662. func (b *tabCloseButton) MouseOut() {
  663. b.hovered = false
  664. b.parent.Refresh()
  665. }
  666. func (b *tabCloseButton) Tapped(*fyne.PointEvent) {
  667. b.onTapped()
  668. }
  669. type tabCloseButtonRenderer struct {
  670. button *tabCloseButton
  671. background *canvas.Rectangle
  672. icon *canvas.Image
  673. objects []fyne.CanvasObject
  674. }
  675. func (r *tabCloseButtonRenderer) Destroy() {
  676. }
  677. func (r *tabCloseButtonRenderer) Layout(size fyne.Size) {
  678. r.background.Resize(size)
  679. r.icon.Resize(size)
  680. }
  681. func (r *tabCloseButtonRenderer) MinSize() fyne.Size {
  682. return fyne.NewSize(theme.IconInlineSize(), theme.IconInlineSize())
  683. }
  684. func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject {
  685. return r.objects
  686. }
  687. func (r *tabCloseButtonRenderer) Refresh() {
  688. if r.button.hovered {
  689. r.background.FillColor = theme.HoverColor()
  690. r.background.Show()
  691. } else {
  692. r.background.Hide()
  693. }
  694. r.background.Refresh()
  695. switch res := r.icon.Resource.(type) {
  696. case *theme.ThemedResource:
  697. if r.button.parent.importance == widget.HighImportance {
  698. r.icon.Resource = theme.NewPrimaryThemedResource(res)
  699. }
  700. case *theme.PrimaryThemedResource:
  701. if r.button.parent.importance != widget.HighImportance {
  702. r.icon.Resource = res.Original()
  703. }
  704. }
  705. r.icon.Refresh()
  706. }
  707. func mismatchedTabItems(items []*TabItem) bool {
  708. var hasText, hasIcon bool
  709. for _, tab := range items {
  710. hasText = hasText || tab.Text != ""
  711. hasIcon = hasIcon || tab.Icon != nil
  712. }
  713. mismatch := false
  714. for _, tab := range items {
  715. if (hasText && tab.Text == "") || (hasIcon && tab.Icon == nil) {
  716. mismatch = true
  717. break
  718. }
  719. }
  720. return mismatch
  721. }
  722. func moreIcon(t baseTabs) fyne.Resource {
  723. if l := t.tabLocation(); l == TabLocationLeading || l == TabLocationTrailing {
  724. return theme.MoreVerticalIcon()
  725. }
  726. return theme.MoreHorizontalIcon()
  727. }