tabs.go 19 KB

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