Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
882 lines
21 KiB
Go
882 lines
21 KiB
Go
package container
|
|
|
|
import (
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/driver/desktop"
|
|
"fyne.io/fyne/v2/internal"
|
|
"fyne.io/fyne/v2/internal/build"
|
|
intTheme "fyne.io/fyne/v2/internal/theme"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
// TabItem represents a single view in a tab view.
|
|
// The Text and Icon are used for the tab button and the Content is shown when the corresponding tab is active.
|
|
//
|
|
// Since: 1.4
|
|
type TabItem struct {
|
|
Text string
|
|
Icon fyne.Resource
|
|
Content fyne.CanvasObject
|
|
|
|
button *tabButton
|
|
|
|
disabled bool
|
|
}
|
|
|
|
// Disabled returns whether or not the TabItem is disabled.
|
|
//
|
|
// Since: 2.3
|
|
func (ti *TabItem) Disabled() bool {
|
|
return ti.disabled
|
|
}
|
|
|
|
func (ti *TabItem) disable() {
|
|
ti.disabled = true
|
|
if ti.button != nil {
|
|
ti.button.Disable()
|
|
}
|
|
}
|
|
|
|
func (ti *TabItem) enable() {
|
|
ti.disabled = false
|
|
if ti.button != nil {
|
|
ti.button.Enable()
|
|
}
|
|
}
|
|
|
|
// TabLocation is the location where the tabs of a tab container should be rendered
|
|
//
|
|
// Since: 1.4
|
|
type TabLocation int
|
|
|
|
// TabLocation values
|
|
const (
|
|
TabLocationTop TabLocation = iota
|
|
TabLocationLeading
|
|
TabLocationBottom
|
|
TabLocationTrailing
|
|
)
|
|
|
|
// NewTabItem creates a new item for a tabbed widget - each item specifies the content and a label for its tab.
|
|
//
|
|
// Since: 1.4
|
|
func NewTabItem(text string, content fyne.CanvasObject) *TabItem {
|
|
return &TabItem{Text: text, Content: content}
|
|
}
|
|
|
|
// NewTabItemWithIcon creates a new item for a tabbed widget - each item specifies the content and a label with an icon for its tab.
|
|
//
|
|
// Since: 1.4
|
|
func NewTabItemWithIcon(text string, icon fyne.Resource, content fyne.CanvasObject) *TabItem {
|
|
return &TabItem{Text: text, Icon: icon, Content: content}
|
|
}
|
|
|
|
type baseTabs interface {
|
|
fyne.Widget
|
|
|
|
onUnselected() func(*TabItem)
|
|
onSelected() func(*TabItem)
|
|
|
|
items() []*TabItem
|
|
setItems([]*TabItem)
|
|
|
|
selected() int
|
|
setSelected(int)
|
|
|
|
tabLocation() TabLocation
|
|
|
|
transitioning() bool
|
|
setTransitioning(bool)
|
|
}
|
|
|
|
func isMobile(b baseTabs) bool {
|
|
d := fyne.CurrentDevice()
|
|
mobile := intTheme.FeatureForWidget(intTheme.FeatureNameDeviceIsMobile, b)
|
|
if is, ok := mobile.(bool); ok {
|
|
return is
|
|
}
|
|
|
|
return d.IsMobile()
|
|
}
|
|
|
|
func tabsAdjustedLocation(l TabLocation, b baseTabs) TabLocation {
|
|
// Mobile has limited screen space, so don't put app tab bar on long edges
|
|
if isMobile(b) {
|
|
if o := fyne.CurrentDevice().Orientation(); fyne.IsVertical(o) {
|
|
if l == TabLocationLeading {
|
|
return TabLocationTop
|
|
} else if l == TabLocationTrailing {
|
|
return TabLocationBottom
|
|
}
|
|
} else {
|
|
if l == TabLocationTop {
|
|
return TabLocationLeading
|
|
} else if l == TabLocationBottom {
|
|
return TabLocationTrailing
|
|
}
|
|
}
|
|
}
|
|
|
|
return l
|
|
}
|
|
|
|
func buildPopUpMenu(t baseTabs, button *widget.Button, items []*fyne.MenuItem) *widget.PopUpMenu {
|
|
d := fyne.CurrentApp().Driver()
|
|
c := d.CanvasForObject(button)
|
|
popUpMenu := widget.NewPopUpMenu(fyne.NewMenu("", items...), c)
|
|
buttonPos := d.AbsolutePositionForObject(button)
|
|
buttonSize := button.Size()
|
|
popUpMin := popUpMenu.MinSize()
|
|
var popUpPos fyne.Position
|
|
switch t.tabLocation() {
|
|
case TabLocationLeading:
|
|
popUpPos.X = buttonPos.X + buttonSize.Width
|
|
popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
|
|
case TabLocationTrailing:
|
|
popUpPos.X = buttonPos.X - popUpMin.Width
|
|
popUpPos.Y = buttonPos.Y + buttonSize.Height - popUpMin.Height
|
|
case TabLocationTop:
|
|
popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
|
|
popUpPos.Y = buttonPos.Y + buttonSize.Height
|
|
case TabLocationBottom:
|
|
popUpPos.X = buttonPos.X + buttonSize.Width - popUpMin.Width
|
|
popUpPos.Y = buttonPos.Y - popUpMin.Height
|
|
}
|
|
if popUpPos.X < 0 {
|
|
popUpPos.X = 0
|
|
}
|
|
if popUpPos.Y < 0 {
|
|
popUpPos.Y = 0
|
|
}
|
|
popUpMenu.ShowAtPosition(popUpPos)
|
|
return popUpMenu
|
|
}
|
|
|
|
func removeIndex(t baseTabs, index int) {
|
|
items := t.items()
|
|
if index < 0 || index >= len(items) {
|
|
return
|
|
}
|
|
setItems(t, append(items[:index], items[index+1:]...))
|
|
if s := t.selected(); index < s {
|
|
t.setSelected(s - 1)
|
|
}
|
|
}
|
|
|
|
func removeItem(t baseTabs, item *TabItem) {
|
|
for index, existingItem := range t.items() {
|
|
if existingItem == item {
|
|
removeIndex(t, index)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func selected(t baseTabs) *TabItem {
|
|
selected := t.selected()
|
|
items := t.items()
|
|
if selected < 0 || selected >= len(items) {
|
|
return nil
|
|
}
|
|
return items[selected]
|
|
}
|
|
|
|
func selectIndex(t baseTabs, index int) {
|
|
selected := t.selected()
|
|
|
|
if selected == index {
|
|
// No change, so do nothing
|
|
return
|
|
}
|
|
|
|
items := t.items()
|
|
|
|
if f := t.onUnselected(); f != nil && selected >= 0 && selected < len(items) {
|
|
// Notification of unselected
|
|
f(items[selected])
|
|
}
|
|
|
|
if index < 0 || index >= len(items) {
|
|
// Out of bounds, so do nothing
|
|
return
|
|
}
|
|
|
|
t.setTransitioning(true)
|
|
t.setSelected(index)
|
|
t.Refresh()
|
|
|
|
if f := t.onSelected(); f != nil {
|
|
// Notification of selected
|
|
f(items[index])
|
|
}
|
|
}
|
|
|
|
func selectItem(t baseTabs, item *TabItem) {
|
|
for i, child := range t.items() {
|
|
if child == item {
|
|
selectIndex(t, i)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func setItems(t baseTabs, items []*TabItem) {
|
|
if build.HasHints && mismatchedTabItems(items) {
|
|
internal.LogHint("Tab items should all have the same type of content (text, icons or both)")
|
|
}
|
|
t.setItems(items)
|
|
selected := t.selected()
|
|
count := len(items)
|
|
switch {
|
|
case count == 0:
|
|
// No items available to be selected
|
|
selectIndex(t, -1) // Unsure OnUnselected gets called if applicable
|
|
t.setSelected(-1)
|
|
case selected < 0:
|
|
// Current is first tab item
|
|
selectIndex(t, 0)
|
|
case selected >= count:
|
|
// Current doesn't exist, select last tab
|
|
selectIndex(t, count-1)
|
|
}
|
|
}
|
|
|
|
func disableIndex(t baseTabs, index int) {
|
|
items := t.items()
|
|
if index < 0 || index >= len(items) {
|
|
return
|
|
}
|
|
|
|
item := items[index]
|
|
item.disable()
|
|
|
|
if selected(t) == item {
|
|
// the disabled tab is currently selected, so select the first enabled tab
|
|
for i, it := range items {
|
|
if !it.Disabled() {
|
|
selectIndex(t, i)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if selected(t) == item {
|
|
selectIndex(t, -1) // no other tab is able to be selected
|
|
}
|
|
}
|
|
|
|
func disableItem(t baseTabs, item *TabItem) {
|
|
for i, it := range t.items() {
|
|
if it == item {
|
|
disableIndex(t, i)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func enableIndex(t baseTabs, index int) {
|
|
items := t.items()
|
|
if index < 0 || index >= len(items) {
|
|
return
|
|
}
|
|
|
|
item := items[index]
|
|
item.enable()
|
|
}
|
|
|
|
func enableItem(t baseTabs, item *TabItem) {
|
|
for i, it := range t.items() {
|
|
if it == item {
|
|
enableIndex(t, i)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
type baseTabsRenderer struct {
|
|
positionAnimation, sizeAnimation *fyne.Animation
|
|
|
|
lastIndicatorPos fyne.Position
|
|
lastIndicatorSize fyne.Size
|
|
lastIndicatorHidden bool
|
|
|
|
action *widget.Button
|
|
bar *fyne.Container
|
|
divider, indicator *canvas.Rectangle
|
|
|
|
tabs baseTabs
|
|
}
|
|
|
|
func (r *baseTabsRenderer) Destroy() {
|
|
}
|
|
|
|
func (r *baseTabsRenderer) applyTheme(t baseTabs) {
|
|
if r.action != nil {
|
|
r.action.SetIcon(moreIcon(t))
|
|
}
|
|
th := theme.CurrentForWidget(t)
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
r.divider.FillColor = th.Color(theme.ColorNameShadow, v)
|
|
r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v)
|
|
r.indicator.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
|
|
|
for _, tab := range r.tabs.items() {
|
|
tab.Content.Refresh()
|
|
}
|
|
}
|
|
|
|
func (r *baseTabsRenderer) layout(t baseTabs, size fyne.Size) {
|
|
var (
|
|
barPos, dividerPos, contentPos fyne.Position
|
|
barSize, dividerSize, contentSize fyne.Size
|
|
)
|
|
|
|
barMin := r.bar.MinSize()
|
|
|
|
th := theme.CurrentForWidget(t)
|
|
padding := th.Size(theme.SizeNamePadding)
|
|
switch t.tabLocation() {
|
|
case TabLocationTop:
|
|
barHeight := barMin.Height
|
|
barPos = fyne.NewPos(0, 0)
|
|
barSize = fyne.NewSize(size.Width, barHeight)
|
|
dividerPos = fyne.NewPos(0, barHeight)
|
|
dividerSize = fyne.NewSize(size.Width, padding)
|
|
contentPos = fyne.NewPos(0, barHeight+padding)
|
|
contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
|
|
case TabLocationLeading:
|
|
barWidth := barMin.Width
|
|
barPos = fyne.NewPos(0, 0)
|
|
barSize = fyne.NewSize(barWidth, size.Height)
|
|
dividerPos = fyne.NewPos(barWidth, 0)
|
|
dividerSize = fyne.NewSize(padding, size.Height)
|
|
contentPos = fyne.NewPos(barWidth+padding, 0)
|
|
contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
|
|
case TabLocationBottom:
|
|
barHeight := barMin.Height
|
|
barPos = fyne.NewPos(0, size.Height-barHeight)
|
|
barSize = fyne.NewSize(size.Width, barHeight)
|
|
dividerPos = fyne.NewPos(0, size.Height-barHeight-padding)
|
|
dividerSize = fyne.NewSize(size.Width, padding)
|
|
contentPos = fyne.NewPos(0, 0)
|
|
contentSize = fyne.NewSize(size.Width, size.Height-barHeight-padding)
|
|
case TabLocationTrailing:
|
|
barWidth := barMin.Width
|
|
barPos = fyne.NewPos(size.Width-barWidth, 0)
|
|
barSize = fyne.NewSize(barWidth, size.Height)
|
|
dividerPos = fyne.NewPos(size.Width-barWidth-padding, 0)
|
|
dividerSize = fyne.NewSize(padding, size.Height)
|
|
contentPos = fyne.NewPos(0, 0)
|
|
contentSize = fyne.NewSize(size.Width-barWidth-padding, size.Height)
|
|
}
|
|
|
|
r.bar.Move(barPos)
|
|
r.bar.Resize(barSize)
|
|
r.divider.Move(dividerPos)
|
|
r.divider.Resize(dividerSize)
|
|
selected := t.selected()
|
|
for i, ti := range t.items() {
|
|
if i == selected {
|
|
ti.Content.Move(contentPos)
|
|
ti.Content.Resize(contentSize)
|
|
ti.Content.Show()
|
|
} else {
|
|
ti.Content.Hide()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *baseTabsRenderer) minSize(t baseTabs) fyne.Size {
|
|
th := theme.CurrentForWidget(t)
|
|
pad := th.Size(theme.SizeNamePadding)
|
|
buttonPad := pad
|
|
barMin := r.bar.MinSize()
|
|
tabsMin := r.bar.Objects[0].MinSize()
|
|
accessory := r.bar.Objects[1]
|
|
accessoryMin := accessory.MinSize()
|
|
if scroll, ok := r.bar.Objects[0].(*Scroll); ok && len(scroll.Content.(*fyne.Container).Objects) == 0 {
|
|
tabsMin = fyne.Size{} // scroller forces 32 where we don't need any space
|
|
buttonPad = 0
|
|
} else if group, ok := r.bar.Objects[0].(*fyne.Container); ok && len(group.Objects) > 0 {
|
|
tabsMin = group.Objects[0].MinSize()
|
|
buttonPad = 0
|
|
}
|
|
if !accessory.Visible() || accessoryMin.Width == 0 {
|
|
buttonPad = 0
|
|
accessoryMin = fyne.Size{}
|
|
}
|
|
|
|
contentMin := fyne.NewSize(0, 0)
|
|
for _, content := range t.items() {
|
|
contentMin = contentMin.Max(content.Content.MinSize())
|
|
}
|
|
|
|
switch t.tabLocation() {
|
|
case TabLocationLeading, TabLocationTrailing:
|
|
return fyne.NewSize(barMin.Width+contentMin.Width+pad,
|
|
fyne.Max(contentMin.Height, accessoryMin.Height+buttonPad+tabsMin.Height))
|
|
default:
|
|
return fyne.NewSize(fyne.Max(contentMin.Width, accessoryMin.Width+buttonPad+tabsMin.Width),
|
|
barMin.Height+contentMin.Height+pad)
|
|
}
|
|
}
|
|
|
|
func (r *baseTabsRenderer) moveIndicator(pos fyne.Position, siz fyne.Size, th fyne.Theme, animate bool) {
|
|
isSameState := r.lastIndicatorPos == pos && r.lastIndicatorSize == siz &&
|
|
r.lastIndicatorHidden == r.indicator.Hidden
|
|
if isSameState {
|
|
return
|
|
}
|
|
|
|
if r.positionAnimation != nil {
|
|
r.positionAnimation.Stop()
|
|
r.positionAnimation = nil
|
|
}
|
|
if r.sizeAnimation != nil {
|
|
r.sizeAnimation.Stop()
|
|
r.sizeAnimation = nil
|
|
}
|
|
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
r.indicator.FillColor = th.Color(theme.ColorNamePrimary, v)
|
|
if r.indicator.Position().IsZero() {
|
|
r.indicator.Move(pos)
|
|
r.indicator.Resize(siz)
|
|
r.indicator.Refresh()
|
|
return
|
|
}
|
|
|
|
r.lastIndicatorPos = pos
|
|
r.lastIndicatorSize = siz
|
|
r.lastIndicatorHidden = r.indicator.Hidden
|
|
|
|
if animate && fyne.CurrentApp().Settings().ShowAnimations() {
|
|
r.positionAnimation = canvas.NewPositionAnimation(r.indicator.Position(), pos, canvas.DurationShort, func(p fyne.Position) {
|
|
r.indicator.Move(p)
|
|
r.indicator.Refresh()
|
|
if pos == p {
|
|
r.positionAnimation.Stop()
|
|
r.positionAnimation = nil
|
|
}
|
|
})
|
|
r.sizeAnimation = canvas.NewSizeAnimation(r.indicator.Size(), siz, canvas.DurationShort, func(s fyne.Size) {
|
|
r.indicator.Resize(s)
|
|
r.indicator.Refresh()
|
|
if siz == s {
|
|
r.sizeAnimation.Stop()
|
|
r.sizeAnimation = nil
|
|
}
|
|
})
|
|
|
|
r.positionAnimation.Start()
|
|
r.sizeAnimation.Start()
|
|
} else {
|
|
r.indicator.Move(pos)
|
|
r.indicator.Resize(siz)
|
|
r.indicator.Refresh()
|
|
}
|
|
}
|
|
|
|
func (r *baseTabsRenderer) objects(t baseTabs) []fyne.CanvasObject {
|
|
objects := []fyne.CanvasObject{r.bar, r.divider, r.indicator}
|
|
if i, is := t.selected(), t.items(); i >= 0 && i < len(is) {
|
|
objects = append(objects, is[i].Content)
|
|
}
|
|
return objects
|
|
}
|
|
|
|
func (r *baseTabsRenderer) refresh(t baseTabs) {
|
|
r.applyTheme(t)
|
|
|
|
r.bar.Refresh()
|
|
r.divider.Refresh()
|
|
r.indicator.Refresh()
|
|
}
|
|
|
|
type buttonIconPosition int
|
|
|
|
const (
|
|
buttonIconInline buttonIconPosition = iota
|
|
buttonIconTop
|
|
)
|
|
|
|
var (
|
|
_ fyne.Widget = (*tabButton)(nil)
|
|
_ fyne.Tappable = (*tabButton)(nil)
|
|
_ desktop.Hoverable = (*tabButton)(nil)
|
|
)
|
|
|
|
type tabButton struct {
|
|
widget.DisableableWidget
|
|
hovered bool
|
|
icon fyne.Resource
|
|
iconPosition buttonIconPosition
|
|
importance widget.Importance
|
|
onTapped func()
|
|
onClosed func()
|
|
text string
|
|
textAlignment fyne.TextAlign
|
|
|
|
tabs baseTabs
|
|
}
|
|
|
|
func (b *tabButton) CreateRenderer() fyne.WidgetRenderer {
|
|
b.ExtendBaseWidget(b)
|
|
th := b.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
|
|
background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
|
background.Hide()
|
|
icon := canvas.NewImageFromResource(b.icon)
|
|
if b.icon == nil {
|
|
icon.Hide()
|
|
}
|
|
|
|
label := canvas.NewText(b.text, th.Color(theme.ColorNameForeground, v))
|
|
label.TextStyle.Bold = true
|
|
|
|
close := &tabCloseButton{
|
|
parent: b,
|
|
onTapped: func() {
|
|
if f := b.onClosed; f != nil {
|
|
f()
|
|
}
|
|
},
|
|
}
|
|
close.ExtendBaseWidget(close)
|
|
close.Hide()
|
|
|
|
objects := []fyne.CanvasObject{background, label, close, icon}
|
|
return &tabButtonRenderer{
|
|
button: b,
|
|
background: background,
|
|
icon: icon,
|
|
label: label,
|
|
close: close,
|
|
objects: objects,
|
|
}
|
|
}
|
|
|
|
func (b *tabButton) MinSize() fyne.Size {
|
|
b.ExtendBaseWidget(b)
|
|
return b.BaseWidget.MinSize()
|
|
}
|
|
|
|
func (b *tabButton) MouseIn(*desktop.MouseEvent) {
|
|
b.hovered = true
|
|
b.Refresh()
|
|
}
|
|
|
|
func (b *tabButton) MouseMoved(*desktop.MouseEvent) {
|
|
}
|
|
|
|
func (b *tabButton) MouseOut() {
|
|
b.hovered = false
|
|
b.Refresh()
|
|
}
|
|
|
|
func (b *tabButton) Tapped(*fyne.PointEvent) {
|
|
if b.Disabled() {
|
|
return
|
|
}
|
|
|
|
b.onTapped()
|
|
}
|
|
|
|
type tabButtonRenderer struct {
|
|
button *tabButton
|
|
background *canvas.Rectangle
|
|
icon *canvas.Image
|
|
label *canvas.Text
|
|
close *tabCloseButton
|
|
objects []fyne.CanvasObject
|
|
}
|
|
|
|
func (r *tabButtonRenderer) Destroy() {
|
|
}
|
|
|
|
func (r *tabButtonRenderer) Layout(size fyne.Size) {
|
|
th := r.button.Theme()
|
|
pad := th.Size(theme.SizeNamePadding)
|
|
r.background.Resize(size)
|
|
padding := r.padding()
|
|
innerSize := size.Subtract(padding)
|
|
innerOffset := fyne.NewPos(padding.Width/2, padding.Height/2)
|
|
labelShift := float32(0)
|
|
if r.icon.Visible() {
|
|
iconSize := r.iconSize()
|
|
var iconOffset fyne.Position
|
|
if r.button.iconPosition == buttonIconTop {
|
|
iconOffset = fyne.NewPos((innerSize.Width-iconSize)/2, 0)
|
|
} else {
|
|
iconOffset = fyne.NewPos(0, (innerSize.Height-iconSize)/2)
|
|
}
|
|
r.icon.Resize(fyne.NewSquareSize(iconSize))
|
|
r.icon.Move(innerOffset.Add(iconOffset))
|
|
labelShift = iconSize + pad
|
|
}
|
|
if r.label.Text != "" {
|
|
var labelOffset fyne.Position
|
|
var labelSize fyne.Size
|
|
if r.button.iconPosition == buttonIconTop {
|
|
labelOffset = fyne.NewPos(0, labelShift)
|
|
labelSize = fyne.NewSize(innerSize.Width, r.label.MinSize().Height)
|
|
} else {
|
|
labelOffset = fyne.NewPos(labelShift, 0)
|
|
labelSize = fyne.NewSize(innerSize.Width-labelShift, innerSize.Height)
|
|
}
|
|
r.label.Resize(labelSize)
|
|
r.label.Move(innerOffset.Add(labelOffset))
|
|
}
|
|
inlineIconSize := th.Size(theme.SizeNameInlineIcon)
|
|
r.close.Move(fyne.NewPos(size.Width-inlineIconSize-pad, (size.Height-inlineIconSize)/2))
|
|
r.close.Resize(fyne.NewSquareSize(inlineIconSize))
|
|
}
|
|
|
|
func (r *tabButtonRenderer) MinSize() fyne.Size {
|
|
th := r.button.Theme()
|
|
var contentWidth, contentHeight float32
|
|
textSize := r.label.MinSize()
|
|
iconSize := r.iconSize()
|
|
padding := th.Size(theme.SizeNamePadding)
|
|
if r.button.iconPosition == buttonIconTop {
|
|
contentWidth = fyne.Max(textSize.Width, iconSize)
|
|
if r.icon.Visible() {
|
|
contentHeight += iconSize
|
|
}
|
|
if r.label.Text != "" {
|
|
if r.icon.Visible() {
|
|
contentHeight += padding
|
|
}
|
|
contentHeight += textSize.Height
|
|
}
|
|
} else {
|
|
contentHeight = fyne.Max(textSize.Height, iconSize)
|
|
if r.icon.Visible() {
|
|
contentWidth += iconSize
|
|
}
|
|
if r.label.Text != "" {
|
|
if r.icon.Visible() {
|
|
contentWidth += padding
|
|
}
|
|
contentWidth += textSize.Width
|
|
}
|
|
}
|
|
if r.button.onClosed != nil {
|
|
inlineIconSize := th.Size(theme.SizeNameInlineIcon)
|
|
contentWidth += inlineIconSize + padding
|
|
contentHeight = fyne.Max(contentHeight, inlineIconSize)
|
|
}
|
|
return fyne.NewSize(contentWidth, contentHeight).Add(r.padding())
|
|
}
|
|
|
|
func (r *tabButtonRenderer) Objects() []fyne.CanvasObject {
|
|
return r.objects
|
|
}
|
|
|
|
func (r *tabButtonRenderer) Refresh() {
|
|
th := r.button.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
if r.button.hovered && !r.button.Disabled() {
|
|
r.background.FillColor = th.Color(theme.ColorNameHover, v)
|
|
r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
|
r.background.Show()
|
|
} else {
|
|
r.background.Hide()
|
|
}
|
|
r.background.Refresh()
|
|
|
|
r.label.Text = r.button.text
|
|
r.label.Alignment = r.button.textAlignment
|
|
if !r.button.Disabled() {
|
|
if r.button.importance == widget.HighImportance {
|
|
r.label.Color = th.Color(theme.ColorNamePrimary, v)
|
|
} else {
|
|
r.label.Color = th.Color(theme.ColorNameForeground, v)
|
|
}
|
|
} else {
|
|
r.label.Color = th.Color(theme.ColorNameDisabled, v)
|
|
}
|
|
r.label.TextSize = th.Size(theme.SizeNameText)
|
|
if r.button.text == "" {
|
|
r.label.Hide()
|
|
} else {
|
|
r.label.Show()
|
|
}
|
|
|
|
r.icon.Resource = r.button.icon
|
|
if r.icon.Resource != nil {
|
|
r.icon.Show()
|
|
switch res := r.icon.Resource.(type) {
|
|
case *theme.ThemedResource:
|
|
if r.button.importance == widget.HighImportance {
|
|
r.icon.Resource = theme.NewPrimaryThemedResource(res)
|
|
}
|
|
case *theme.PrimaryThemedResource:
|
|
if r.button.importance != widget.HighImportance {
|
|
r.icon.Resource = res.Original()
|
|
}
|
|
}
|
|
r.icon.Refresh()
|
|
} else {
|
|
r.icon.Hide()
|
|
}
|
|
|
|
if r.button.onClosed != nil && (isMobile(r.button.tabs) || r.button.hovered || r.close.hovered) {
|
|
r.close.Show()
|
|
} else {
|
|
r.close.Hide()
|
|
}
|
|
r.close.Refresh()
|
|
|
|
canvas.Refresh(r.button)
|
|
}
|
|
|
|
func (r *tabButtonRenderer) iconSize() float32 {
|
|
iconSize := r.button.Theme().Size(theme.SizeNameInlineIcon)
|
|
if r.button.iconPosition == buttonIconTop {
|
|
return 2 * iconSize
|
|
}
|
|
|
|
return iconSize
|
|
}
|
|
|
|
func (r *tabButtonRenderer) padding() fyne.Size {
|
|
padding := r.button.Theme().Size(theme.SizeNameInnerPadding)
|
|
if r.label.Text != "" && r.button.iconPosition == buttonIconInline {
|
|
return fyne.NewSquareSize(padding * 2)
|
|
}
|
|
return fyne.NewSize(padding, padding*2)
|
|
}
|
|
|
|
var (
|
|
_ fyne.Widget = (*tabCloseButton)(nil)
|
|
_ fyne.Tappable = (*tabCloseButton)(nil)
|
|
_ desktop.Hoverable = (*tabCloseButton)(nil)
|
|
)
|
|
|
|
type tabCloseButton struct {
|
|
widget.BaseWidget
|
|
parent *tabButton
|
|
hovered bool
|
|
onTapped func()
|
|
}
|
|
|
|
func (b *tabCloseButton) CreateRenderer() fyne.WidgetRenderer {
|
|
b.ExtendBaseWidget(b)
|
|
th := b.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
background := canvas.NewRectangle(th.Color(theme.ColorNameHover, v))
|
|
background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
|
background.Hide()
|
|
icon := canvas.NewImageFromResource(theme.CancelIcon())
|
|
|
|
return &tabCloseButtonRenderer{
|
|
button: b,
|
|
background: background,
|
|
icon: icon,
|
|
objects: []fyne.CanvasObject{background, icon},
|
|
}
|
|
}
|
|
|
|
func (b *tabCloseButton) MinSize() fyne.Size {
|
|
b.ExtendBaseWidget(b)
|
|
return b.BaseWidget.MinSize()
|
|
}
|
|
|
|
func (b *tabCloseButton) MouseIn(*desktop.MouseEvent) {
|
|
b.hovered = true
|
|
b.parent.Refresh()
|
|
}
|
|
|
|
func (b *tabCloseButton) MouseMoved(*desktop.MouseEvent) {
|
|
}
|
|
|
|
func (b *tabCloseButton) MouseOut() {
|
|
b.hovered = false
|
|
b.parent.Refresh()
|
|
}
|
|
|
|
func (b *tabCloseButton) Tapped(*fyne.PointEvent) {
|
|
b.onTapped()
|
|
}
|
|
|
|
type tabCloseButtonRenderer struct {
|
|
button *tabCloseButton
|
|
background *canvas.Rectangle
|
|
icon *canvas.Image
|
|
objects []fyne.CanvasObject
|
|
}
|
|
|
|
func (r *tabCloseButtonRenderer) Destroy() {
|
|
}
|
|
|
|
func (r *tabCloseButtonRenderer) Layout(size fyne.Size) {
|
|
r.background.Resize(size)
|
|
r.icon.Resize(size)
|
|
}
|
|
|
|
func (r *tabCloseButtonRenderer) MinSize() fyne.Size {
|
|
return fyne.NewSquareSize(r.button.Theme().Size(theme.SizeNameInlineIcon))
|
|
}
|
|
|
|
func (r *tabCloseButtonRenderer) Objects() []fyne.CanvasObject {
|
|
return r.objects
|
|
}
|
|
|
|
func (r *tabCloseButtonRenderer) Refresh() {
|
|
th := r.button.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
if r.button.hovered {
|
|
r.background.FillColor = th.Color(theme.ColorNameHover, v)
|
|
r.background.CornerRadius = th.Size(theme.SizeNameSelectionRadius)
|
|
r.background.Show()
|
|
} else {
|
|
r.background.Hide()
|
|
}
|
|
r.background.Refresh()
|
|
switch res := r.icon.Resource.(type) {
|
|
case *theme.ThemedResource:
|
|
if r.button.parent.importance == widget.HighImportance {
|
|
r.icon.Resource = theme.NewPrimaryThemedResource(res)
|
|
}
|
|
case *theme.PrimaryThemedResource:
|
|
if r.button.parent.importance != widget.HighImportance {
|
|
r.icon.Resource = res.Original()
|
|
}
|
|
}
|
|
r.icon.Refresh()
|
|
}
|
|
|
|
func mismatchedTabItems(items []*TabItem) bool {
|
|
var hasText, hasIcon bool
|
|
for _, tab := range items {
|
|
hasText = hasText || tab.Text != ""
|
|
hasIcon = hasIcon || tab.Icon != nil
|
|
}
|
|
|
|
mismatch := false
|
|
for _, tab := range items {
|
|
if (hasText && tab.Text == "") || (hasIcon && tab.Icon == nil) {
|
|
mismatch = true
|
|
break
|
|
}
|
|
}
|
|
|
|
return mismatch
|
|
}
|
|
|
|
func moreIcon(t baseTabs) fyne.Resource {
|
|
if l := t.tabLocation(); l == TabLocationLeading || l == TabLocationTrailing {
|
|
return theme.MoreVerticalIcon()
|
|
}
|
|
return theme.MoreHorizontalIcon()
|
|
}
|