tabs.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843
  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 internal.HintsEnabled && 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. r.indicator.CornerRadius = theme.SelectionRadiusSize()
  263. }
  264. func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
  265. var (
  266. barPos, dividerPos, contentPos fyne.Position
  267. barSize, dividerSize, contentSize fyne.Size
  268. )
  269. barMin := r.bar.MinSize()
  270. padding := theme.Padding()
  271. switch t.tabLocation() {
  272. case TabLocationTop:
  273. barHeight := barMin.Height
  274. barPos = fyne.NewPos(0, 0)
  275. barSize = fyne.NewSize(size.Width, barHeight)
  276. dividerPos = fyne.NewPos(0, barHeight)
  277. dividerSize = fyne.NewSize(size.Width, padding)
  278. contentPos = fyne.NewPos(0, barHeight+padding)
  279. contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
  280. case TabLocationLeading:
  281. barWidth := barMin.Width
  282. barPos = fyne.NewPos(0, 0)
  283. barSize = fyne.NewSize(barWidth, size.Height)
  284. dividerPos = fyne.NewPos(barWidth, 0)
  285. dividerSize = fyne.NewSize(padding, size.Height)
  286. contentPos = fyne.NewPos(barWidth+theme.Padding(), 0)
  287. contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
  288. case TabLocationBottom:
  289. barHeight := barMin.Height
  290. barPos = fyne.NewPos(0, size.Height-barHeight)
  291. barSize = fyne.NewSize(size.Width, barHeight)
  292. dividerPos = fyne.NewPos(0, size.Height-barHeight-padding)
  293. dividerSize = fyne.NewSize(size.Width, padding)
  294. contentPos = fyne.NewPos(0, 0)
  295. contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
  296. case TabLocationTrailing:
  297. barWidth := barMin.Width
  298. barPos = fyne.NewPos(size.Width-barWidth, 0)
  299. barSize = fyne.NewSize(barWidth, size.Height)
  300. dividerPos = fyne.NewPos(size.Width-barWidth-padding, 0)
  301. dividerSize = fyne.NewSize(padding, size.Height)
  302. contentPos = fyne.NewPos(0, 0)
  303. contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
  304. }
  305. r.bar.Move(barPos)
  306. r.bar.Resize(barSize)
  307. r.divider.Move(dividerPos)
  308. r.divider.Resize(dividerSize)
  309. selected := t.selected()
  310. for i, ti := range t.items() {
  311. if i == selected {
  312. ti.Content.Move(contentPos)
  313. ti.Content.Resize(contentSize)
  314. ti.Content.Show()
  315. } else {
  316. ti.Content.Hide()
  317. }
  318. }
  319. }
  320. func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size {
  321. pad := theme.Padding()
  322. buttonPad := pad
  323. barMin := r.bar.MinSize()
  324. tabsMin := r.bar.Objects[0].MinSize()
  325. accessory := r.bar.Objects[1]
  326. accessoryMin := accessory.MinSize()
  327. if scroll, ok := r.bar.Objects[0].(*Scroll); ok && len(scroll.Content.(*fyne.Container).Objects) == 0 {
  328. tabsMin = fyne.Size{} // scroller forces 32 where we don't need any space
  329. buttonPad = 0
  330. } else if group, ok := r.bar.Objects[0].(*fyne.Container); ok && len(group.Objects) > 0 {
  331. tabsMin = group.Objects[0].MinSize()
  332. buttonPad = 0
  333. }
  334. if !accessory.Visible() || accessoryMin.Width == 0 {
  335. buttonPad = 0
  336. accessoryMin = fyne.Size{}
  337. }
  338. contentMin := fyne.NewSize(0, 0)
  339. for _, content := range t.items() {
  340. contentMin = contentMin.Max(content.Content.MinSize())
  341. }
  342. switch t.tabLocation() {
  343. case TabLocationLeading, TabLocationTrailing:
  344. return fyne.NewSize(barMin.Width+contentMin.Width+pad,
  345. fyne.Max(contentMin.Height, accessoryMin.Height+buttonPad+tabsMin.Height))
  346. default:
  347. return fyne.NewSize(fyne.Max(contentMin.Width, accessoryMin.Width+buttonPad+tabsMin.Width),
  348. barMin.Height+contentMin.Height+pad)
  349. }
  350. }
  351. func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, animate bool) {
  352. r.lastIndicatorMutex.RLock()
  353. isSameState := r.lastIndicatorPos.Subtract(pos).IsZero() && r.lastIndicatorSize.Subtract(siz).IsZero() &&
  354. r.lastIndicatorHidden == r.indicator.Hidden
  355. r.lastIndicatorMutex.RUnlock()
  356. if isSameState {
  357. return
  358. }
  359. if r.positionAnimation != nil {
  360. r.positionAnimation.Stop()
  361. r.positionAnimation = nil
  362. }
  363. if r.sizeAnimation != nil {
  364. r.sizeAnimation.Stop()
  365. r.sizeAnimation = nil
  366. }
  367. r.indicator.FillColor = theme.PrimaryColor()
  368. if r.indicator.Position().IsZero() {
  369. r.indicator.Move(pos)
  370. r.indicator.Resize(siz)
  371. r.indicator.Refresh()
  372. return
  373. }
  374. r.lastIndicatorMutex.Lock()
  375. r.lastIndicatorPos = pos
  376. r.lastIndicatorSize = siz
  377. r.lastIndicatorHidden = r.indicator.Hidden
  378. r.lastIndicatorMutex.Unlock()
  379. if animate && fyne.CurrentApp().Settings().ShowAnimations() {
  380. r.positionAnimation = canvas.NewPositionAnimation(r.indicator.Position(), pos, canvas.DurationShort, func(p fyne.Position) {
  381. r.indicator.Move(p)
  382. r.indicator.Refresh()
  383. if pos == p {
  384. r.positionAnimation.Stop()
  385. r.positionAnimation = nil
  386. }
  387. })
  388. r.sizeAnimation = canvas.NewSizeAnimation(r.indicator.Size(), siz, canvas.DurationShort, func(s fyne.Size) {
  389. r.indicator.Resize(s)
  390. r.indicator.Refresh()
  391. if siz == s {
  392. r.sizeAnimation.Stop()
  393. r.sizeAnimation = nil
  394. }
  395. })
  396. r.positionAnimation.Start()
  397. r.sizeAnimation.Start()
  398. } else {
  399. r.indicator.Move(pos)
  400. r.indicator.Resize(siz)
  401. r.indicator.Refresh()
  402. }
  403. }
  404. func (r *baseTabsRenderer) objects(t baseTabs) []fyne.CanvasObject {
  405. objects := []fyne.CanvasObject{r.bar, r.divider, r.indicator}
  406. if i, is := t.selected(), t.items(); i >= 0 && i < len(is) {
  407. objects = append(objects, is[i].Content)
  408. }
  409. return objects
  410. }
  411. func (r *baseTabsRenderer) refresh(t baseTabs) {
  412. r.applyTheme(t)
  413. r.bar.Refresh()
  414. r.divider.Refresh()
  415. r.indicator.Refresh()
  416. }
  417. type buttonIconPosition int
  418. const (
  419. buttonIconInline buttonIconPosition = iota
  420. buttonIconTop
  421. )
  422. var _ fyne.Widget = (*tabButton)(nil)
  423. var _ fyne.Tappable = (*tabButton)(nil)
  424. var _ desktop.Hoverable = (*tabButton)(nil)
  425. type tabButton struct {
  426. widget.DisableableWidget
  427. hovered bool
  428. icon fyne.Resource
  429. iconPosition buttonIconPosition
  430. importance widget.Importance
  431. onTapped func()
  432. onClosed func()
  433. text string
  434. textAlignment fyne.TextAlign
  435. }
  436. func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
  437. b.ExtendBaseWidget(b)
  438. background := canvas.NewRectangle(theme.HoverColor())
  439. background.CornerRadius = theme.SelectionRadiusSize()
  440. background.Hide()
  441. icon := canvas.NewImageFromResource(b.icon)
  442. if b.icon == nil {
  443. icon.Hide()
  444. }
  445. label := canvas.NewText(b.text, theme.ForegroundColor())
  446. label.TextStyle.Bold = true
  447. close := &tabCloseButton{
  448. parent: b,
  449. onTapped: func() {
  450. if f := b.onClosed; f != nil {
  451. f()
  452. }
  453. },
  454. }
  455. close.ExtendBaseWidget(close)
  456. close.Hide()
  457. objects := []fyne.CanvasObject{background, label, close, icon}
  458. r := &tabButtonRenderer{
  459. button: b,
  460. background: background,
  461. icon: icon,
  462. label: label,
  463. close: close,
  464. objects: objects,
  465. }
  466. r.Refresh()
  467. return r
  468. }
  469. func (b *tabButton) MinSize() fyne.Size {
  470. b.ExtendBaseWidget(b)
  471. return b.BaseWidget.MinSize()
  472. }
  473. func (b *tabButton) MouseIn(*desktop.MouseEvent) {
  474. b.hovered = true
  475. b.Refresh()
  476. }
  477. func (b *tabButton) MouseMoved(*desktop.MouseEvent) {
  478. }
  479. func (b *tabButton) MouseOut() {
  480. b.hovered = false
  481. b.Refresh()
  482. }
  483. func (b *tabButton) Tapped(*fyne.PointEvent) {
  484. if b.Disabled() {
  485. return
  486. }
  487. b.onTapped()
  488. }
  489. type tabButtonRenderer struct {
  490. button *tabButton
  491. background *canvas.Rectangle
  492. icon *canvas.Image
  493. label *canvas.Text
  494. close *tabCloseButton
  495. objects []fyne.CanvasObject
  496. }
  497. func (r *tabButtonRenderer) Destroy() {
  498. }
  499. func (r *tabButtonRenderer) Layout(size fyne.Size) {
  500. r.background.Resize(size)
  501. padding := r.padding()
  502. innerSize := size.Subtract(padding)
  503. innerOffset := fyne.NewPos(padding.Width/2, padding.Height/2)
  504. labelShift := float32(0)
  505. if r.icon.Visible() {
  506. iconSize := r.iconSize()
  507. var iconOffset fyne.Position
  508. if r.button.iconPosition == buttonIconTop {
  509. iconOffset = fyne.NewPos((innerSize.Width-iconSize)/2, 0)
  510. } else {
  511. iconOffset = fyne.NewPos(0, (innerSize.Height-iconSize)/2)
  512. }
  513. r.icon.Resize(fyne.NewSquareSize(iconSize))
  514. r.icon.Move(innerOffset.Add(iconOffset))
  515. labelShift = iconSize + theme.Padding()
  516. }
  517. if r.label.Text != "" {
  518. var labelOffset fyne.Position
  519. var labelSize fyne.Size
  520. if r.button.iconPosition == buttonIconTop {
  521. labelOffset = fyne.NewPos(0, labelShift)
  522. labelSize = fyne.NewSize(innerSize.Width, r.label.MinSize().Height)
  523. } else {
  524. labelOffset = fyne.NewPos(labelShift, 0)
  525. labelSize = fyne.NewSize(innerSize.Width-labelShift, innerSize.Height)
  526. }
  527. r.label.Resize(labelSize)
  528. r.label.Move(innerOffset.Add(labelOffset))
  529. }
  530. inlineIconSize := theme.IconInlineSize()
  531. r.close.Move(fyne.NewPos(size.Width-inlineIconSize-theme.Padding(), (size.Height-inlineIconSize)/2))
  532. r.close.Resize(fyne.NewSquareSize(inlineIconSize))
  533. }
  534. func (r *tabButtonRenderer) MinSize() fyne.Size {
  535. var contentWidth, contentHeight float32
  536. textSize := r.label.MinSize()
  537. iconSize := r.iconSize()
  538. padding := theme.Padding()
  539. if r.button.iconPosition == buttonIconTop {
  540. contentWidth = fyne.Max(textSize.Width, iconSize)
  541. if r.icon.Visible() {
  542. contentHeight += iconSize
  543. }
  544. if r.label.Text != "" {
  545. if r.icon.Visible() {
  546. contentHeight += padding
  547. }
  548. contentHeight += textSize.Height
  549. }
  550. } else {
  551. contentHeight = fyne.Max(textSize.Height, iconSize)
  552. if r.icon.Visible() {
  553. contentWidth += iconSize
  554. }
  555. if r.label.Text != "" {
  556. if r.icon.Visible() {
  557. contentWidth += padding
  558. }
  559. contentWidth += textSize.Width
  560. }
  561. }
  562. if r.button.onClosed != nil {
  563. inlineIconSize := theme.IconInlineSize()
  564. contentWidth += inlineIconSize + padding
  565. contentHeight = fyne.Max(contentHeight, inlineIconSize)
  566. }
  567. return fyne.NewSize(contentWidth, contentHeight).Add(r.padding())
  568. }
  569. func (r *tabButtonRenderer) Objects() []fyne.CanvasObject {
  570. return r.objects
  571. }
  572. func (r *tabButtonRenderer) Refresh() {
  573. if r.button.hovered && !r.button.Disabled() {
  574. r.background.FillColor = theme.HoverColor()
  575. r.background.CornerRadius = theme.SelectionRadiusSize()
  576. r.background.Show()
  577. } else {
  578. r.background.Hide()
  579. }
  580. r.background.Refresh()
  581. r.label.Text = r.button.text
  582. r.label.Alignment = r.button.textAlignment
  583. if !r.button.Disabled() {
  584. if r.button.importance == widget.HighImportance {
  585. r.label.Color = theme.PrimaryColor()
  586. } else {
  587. r.label.Color = theme.ForegroundColor()
  588. }
  589. } else {
  590. r.label.Color = theme.DisabledColor()
  591. }
  592. r.label.TextSize = theme.TextSize()
  593. if r.button.text == "" {
  594. r.label.Hide()
  595. } else {
  596. r.label.Show()
  597. }
  598. r.icon.Resource = r.button.icon
  599. if r.icon.Resource != nil {
  600. r.icon.Show()
  601. switch res := r.icon.Resource.(type) {
  602. case *theme.ThemedResource:
  603. if r.button.importance == widget.HighImportance {
  604. r.icon.Resource = theme.NewPrimaryThemedResource(res)
  605. r.icon.Refresh()
  606. }
  607. case *theme.PrimaryThemedResource:
  608. if r.button.importance != widget.HighImportance {
  609. r.icon.Resource = res.Original()
  610. r.icon.Refresh()
  611. }
  612. }
  613. } else {
  614. r.icon.Hide()
  615. }
  616. if d := fyne.CurrentDevice(); r.button.onClosed != nil && (d.IsMobile() || r.button.hovered || r.close.hovered) {
  617. r.close.Show()
  618. } else {
  619. r.close.Hide()
  620. }
  621. r.close.Refresh()
  622. canvas.Refresh(r.button)
  623. }
  624. func (r *tabButtonRenderer) iconSize() float32 {
  625. if r.button.iconPosition == buttonIconTop {
  626. return 2 * theme.IconInlineSize()
  627. }
  628. return theme.IconInlineSize()
  629. }
  630. func (r *tabButtonRenderer) padding() fyne.Size {
  631. padding := theme.InnerPadding()
  632. if r.label.Text != "" && r.button.iconPosition == buttonIconInline {
  633. return fyne.NewSquareSize(padding * 2)
  634. }
  635. return fyne.NewSize(padding, padding*2)
  636. }
  637. var _ fyne.Widget = (*tabCloseButton)(nil)
  638. var _ fyne.Tappable = (*tabCloseButton)(nil)
  639. var _ desktop.Hoverable = (*tabCloseButton)(nil)
  640. type tabCloseButton struct {
  641. widget.BaseWidget
  642. parent *tabButton
  643. hovered bool
  644. onTapped func()
  645. }
  646. func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer {
  647. b.ExtendBaseWidget(b)
  648. background := canvas.NewRectangle(theme.HoverColor())
  649. background.CornerRadius = theme.SelectionRadiusSize()
  650. background.Hide()
  651. icon := canvas.NewImageFromResource(theme.CancelIcon())
  652. r := &tabCloseButtonRenderer{
  653. button: b,
  654. background: background,
  655. icon: icon,
  656. objects: []fyne.CanvasObject{background, icon},
  657. }
  658. r.Refresh()
  659. return r
  660. }
  661. func (b *tabCloseButton) MinSize() fyne.Size {
  662. b.ExtendBaseWidget(b)
  663. return b.BaseWidget.MinSize()
  664. }
  665. func (b *tabCloseButton) MouseIn(*desktop.MouseEvent) {
  666. b.hovered = true
  667. b.parent.Refresh()
  668. }
  669. func (b *tabCloseButton) MouseMoved(*desktop.MouseEvent) {
  670. }
  671. func (b *tabCloseButton) MouseOut() {
  672. b.hovered = false
  673. b.parent.Refresh()
  674. }
  675. func (b *tabCloseButton) Tapped(*fyne.PointEvent) {
  676. b.onTapped()
  677. }
  678. type tabCloseButtonRenderer struct {
  679. button *tabCloseButton
  680. background *canvas.Rectangle
  681. icon *canvas.Image
  682. objects []fyne.CanvasObject
  683. }
  684. func (r *tabCloseButtonRenderer) Destroy() {
  685. }
  686. func (r *tabCloseButtonRenderer) Layout(size fyne.Size) {
  687. r.background.Resize(size)
  688. r.icon.Resize(size)
  689. }
  690. func (r *tabCloseButtonRenderer) MinSize() fyne.Size {
  691. return fyne.NewSquareSize(theme.IconInlineSize())
  692. }
  693. func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject {
  694. return r.objects
  695. }
  696. func (r *tabCloseButtonRenderer) Refresh() {
  697. if r.button.hovered {
  698. r.background.FillColor = theme.HoverColor()
  699. r.background.CornerRadius = theme.SelectionRadiusSize()
  700. r.background.Show()
  701. } else {
  702. r.background.Hide()
  703. }
  704. r.background.Refresh()
  705. switch res := r.icon.Resource.(type) {
  706. case *theme.ThemedResource:
  707. if r.button.parent.importance == widget.HighImportance {
  708. r.icon.Resource = theme.NewPrimaryThemedResource(res)
  709. }
  710. case *theme.PrimaryThemedResource:
  711. if r.button.parent.importance != widget.HighImportance {
  712. r.icon.Resource = res.Original()
  713. }
  714. }
  715. r.icon.Refresh()
  716. }
  717. func mismatchedTabItems(items []*TabItem) bool {
  718. var hasText, hasIcon bool
  719. for _, tab := range items {
  720. hasText = hasText || tab.Text != ""
  721. hasIcon = hasIcon || tab.Icon != nil
  722. }
  723. mismatch := false
  724. for _, tab := range items {
  725. if (hasText && tab.Text == "") || (hasIcon && tab.Icon == nil) {
  726. mismatch = true
  727. break
  728. }
  729. }
  730. return mismatch
  731. }
  732. func moreIcon(t baseTabs) fyne.Resource {
  733. if l := t.tabLocation(); l == TabLocationLeading || l == TabLocationTrailing {
  734. return theme.MoreVerticalIcon()
  735. }
  736. return theme.MoreHorizontalIcon()
  737. }