VideoTools/vendor/fyne.io/fyne/v2/container/apptabs.go
Stu Leak 68df790d27 Fix player frame generation and video playback
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
2026-01-07 22:20:00 -05:00

471 lines
12 KiB
Go

package container
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// Declare conformity with Widget interface.
var _ fyne.Widget = (*AppTabs)(nil)
// AppTabs container is used to split your application into various different areas identified by tabs.
// The tabs contain text and/or an icon and allow the user to switch between the content specified in each TabItem.
// Each item is represented by a button at the edge of the container.
//
// Since: 1.4
type AppTabs struct {
widget.BaseWidget
Items []*TabItem
// Deprecated: Use `OnSelected func(*TabItem)` instead.
OnChanged func(*TabItem) `json:"-"`
OnSelected func(*TabItem) `json:"-"`
OnUnselected func(*TabItem) `json:"-"`
current int
location TabLocation
isTransitioning bool
popUpMenu *widget.PopUpMenu
}
// NewAppTabs creates a new tab container that allows the user to choose between different areas of an app.
//
// Since: 1.4
func NewAppTabs(items ...*TabItem) *AppTabs {
tabs := &AppTabs{Items: items}
tabs.BaseWidget.ExtendBaseWidget(tabs)
return tabs
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (t *AppTabs) CreateRenderer() fyne.WidgetRenderer {
t.BaseWidget.ExtendBaseWidget(t)
th := t.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
r := &appTabsRenderer{
baseTabsRenderer: baseTabsRenderer{
bar: &fyne.Container{},
divider: canvas.NewRectangle(th.Color(theme.ColorNameShadow, v)),
indicator: canvas.NewRectangle(th.Color(theme.ColorNamePrimary, v)),
},
appTabs: t,
}
r.action = r.buildOverflowTabsButton()
r.tabs = t
// Initially setup the tab bar to only show one tab, all others will be in overflow.
// When the widget is laid out, and we know the size, the tab bar will be updated to show as many as can fit.
r.updateTabs(1)
r.updateIndicator(false)
r.applyTheme(t)
return r
}
// Append adds a new TabItem to the end of the tab bar.
func (t *AppTabs) Append(item *TabItem) {
t.SetItems(append(t.Items, item))
}
// CurrentTab returns the currently selected TabItem.
//
// Deprecated: Use `AppTabs.Selected() *TabItem` instead.
func (t *AppTabs) CurrentTab() *TabItem {
if t.current < 0 || t.current >= len(t.Items) {
return nil
}
return t.Items[t.current]
}
// CurrentTabIndex returns the index of the currently selected TabItem.
//
// Deprecated: Use `AppTabs.SelectedIndex() int` instead.
func (t *AppTabs) CurrentTabIndex() int {
return t.SelectedIndex()
}
// DisableIndex disables the TabItem at the specified index.
//
// Since: 2.3
func (t *AppTabs) DisableIndex(i int) {
disableIndex(t, i)
}
// DisableItem disables the specified TabItem.
//
// Since: 2.3
func (t *AppTabs) DisableItem(item *TabItem) {
disableItem(t, item)
}
// EnableIndex enables the TabItem at the specified index.
//
// Since: 2.3
func (t *AppTabs) EnableIndex(i int) {
enableIndex(t, i)
}
// EnableItem enables the specified TabItem.
//
// Since: 2.3
func (t *AppTabs) EnableItem(item *TabItem) {
enableItem(t, item)
}
// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
//
// Deprecated: Support for extending containers is being removed
func (t *AppTabs) ExtendBaseWidget(wid fyne.Widget) {
t.BaseWidget.ExtendBaseWidget(wid)
}
// Hide hides the widget.
func (t *AppTabs) Hide() {
if t.popUpMenu != nil {
t.popUpMenu.Hide()
t.popUpMenu = nil
}
t.BaseWidget.Hide()
}
// MinSize returns the size that this widget should not shrink below
func (t *AppTabs) MinSize() fyne.Size {
t.BaseWidget.ExtendBaseWidget(t)
return t.BaseWidget.MinSize()
}
// Remove tab by value.
func (t *AppTabs) Remove(item *TabItem) {
removeItem(t, item)
t.Refresh()
}
// RemoveIndex removes tab by index.
func (t *AppTabs) RemoveIndex(index int) {
removeIndex(t, index)
t.Refresh()
}
// Select sets the specified TabItem to be selected and its content visible.
func (t *AppTabs) Select(item *TabItem) {
selectItem(t, item)
}
// SelectIndex sets the TabItem at the specific index to be selected and its content visible.
func (t *AppTabs) SelectIndex(index int) {
selectIndex(t, index)
}
// SelectTab sets the specified TabItem to be selected and its content visible.
//
// Deprecated: Use `AppTabs.Select(*TabItem)` instead.
func (t *AppTabs) SelectTab(item *TabItem) {
for i, child := range t.Items {
if child == item {
t.SelectTabIndex(i)
return
}
}
}
// SelectTabIndex sets the TabItem at the specific index to be selected and its content visible.
//
// Deprecated: Use `AppTabs.SelectIndex(int)` instead.
func (t *AppTabs) SelectTabIndex(index int) {
if index < 0 || index >= len(t.Items) || t.current == index {
return
}
t.current = index
t.Refresh()
if t.OnChanged != nil {
t.OnChanged(t.Items[t.current])
}
}
// Selected returns the currently selected TabItem.
func (t *AppTabs) Selected() *TabItem {
return selected(t)
}
// SelectedIndex returns the index of the currently selected TabItem.
func (t *AppTabs) SelectedIndex() int {
return t.selected()
}
// SetItems sets the containers items and refreshes.
func (t *AppTabs) SetItems(items []*TabItem) {
setItems(t, items)
t.Refresh()
}
// SetTabLocation sets the location of the tab bar
func (t *AppTabs) SetTabLocation(l TabLocation) {
t.location = tabsAdjustedLocation(l, t)
t.Refresh()
}
// Show this widget, if it was previously hidden
func (t *AppTabs) Show() {
t.BaseWidget.Show()
t.SelectIndex(t.current)
}
func (t *AppTabs) onUnselected() func(*TabItem) {
return t.OnUnselected
}
func (t *AppTabs) onSelected() func(*TabItem) {
return func(tab *TabItem) {
if f := t.OnChanged; f != nil {
f(tab)
}
if f := t.OnSelected; f != nil {
f(tab)
}
}
}
func (t *AppTabs) items() []*TabItem {
return t.Items
}
func (t *AppTabs) selected() int {
if len(t.Items) == 0 {
return -1
}
return t.current
}
func (t *AppTabs) setItems(items []*TabItem) {
t.Items = items
}
func (t *AppTabs) setSelected(selected int) {
t.current = selected
}
func (t *AppTabs) setTransitioning(transitioning bool) {
t.isTransitioning = transitioning
}
func (t *AppTabs) tabLocation() TabLocation {
return t.location
}
func (t *AppTabs) transitioning() bool {
return t.isTransitioning
}
// Declare conformity with WidgetRenderer interface.
var _ fyne.WidgetRenderer = (*appTabsRenderer)(nil)
type appTabsRenderer struct {
baseTabsRenderer
appTabs *AppTabs
}
func (r *appTabsRenderer) Layout(size fyne.Size) {
// Try render as many tabs as will fit, others will appear in the overflow
if len(r.appTabs.Items) == 0 {
r.updateTabs(0)
} else {
for i := len(r.appTabs.Items); i > 0; i-- {
r.updateTabs(i)
barMin := r.bar.MinSize()
if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
if barMin.Height <= size.Height {
// Tab bar is short enough to fit
break
}
} else {
if barMin.Width <= size.Width {
// Tab bar is thin enough to fit
break
}
}
}
}
r.layout(r.appTabs, size)
r.updateIndicator(r.appTabs.transitioning())
if r.appTabs.transitioning() {
r.appTabs.setTransitioning(false)
}
}
func (r *appTabsRenderer) MinSize() fyne.Size {
return r.minSize(r.appTabs)
}
func (r *appTabsRenderer) Objects() []fyne.CanvasObject {
return r.objects(r.appTabs)
}
func (r *appTabsRenderer) Refresh() {
r.Layout(r.appTabs.Size())
r.refresh(r.appTabs)
canvas.Refresh(r.appTabs)
}
func (r *appTabsRenderer) buildOverflowTabsButton() (overflow *widget.Button) {
overflow = &widget.Button{Icon: moreIcon(r.appTabs), Importance: widget.LowImportance, OnTapped: func() {
// Show pop up containing all tabs which did not fit in the tab bar
itemLen, objLen := len(r.appTabs.Items), len(r.bar.Objects[0].(*fyne.Container).Objects)
items := make([]*fyne.MenuItem, 0, itemLen-objLen)
for i := objLen; i < itemLen; i++ {
index := i // capture
// FIXME MenuItem doesn't support icons (#1752)
// FIXME MenuItem can't show if it is the currently selected tab (#1753)
ti := r.appTabs.Items[i]
mi := fyne.NewMenuItem(ti.Text, func() {
r.appTabs.SelectIndex(index)
if r.appTabs.popUpMenu != nil {
r.appTabs.popUpMenu.Hide()
r.appTabs.popUpMenu = nil
}
})
if ti.Disabled() {
mi.Disabled = true
}
items = append(items, mi)
}
r.appTabs.popUpMenu = buildPopUpMenu(r.appTabs, overflow, items)
}}
return overflow
}
func (r *appTabsRenderer) buildTabButtons(count int) *fyne.Container {
buttons := &fyne.Container{}
var iconPos buttonIconPosition
if isMobile(r.tabs) {
cells := count
if cells == 0 {
cells = 1
}
if r.appTabs.location == TabLocationTop || r.appTabs.location == TabLocationBottom {
buttons.Layout = layout.NewGridLayoutWithColumns(cells)
} else {
buttons.Layout = layout.NewGridLayoutWithRows(cells)
}
iconPos = buttonIconTop
} else if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
buttons.Layout = layout.NewVBoxLayout()
iconPos = buttonIconTop
} else {
buttons.Layout = layout.NewHBoxLayout()
iconPos = buttonIconInline
}
for i := 0; i < count; i++ {
item := r.appTabs.Items[i]
if item.button == nil {
item.button = &tabButton{
onTapped: func() { r.appTabs.Select(item) },
tabs: r.tabs,
}
if item.disabled {
item.button.Disable()
}
}
button := item.button
button.icon = item.Icon
button.iconPosition = iconPos
if i == r.appTabs.current {
button.importance = widget.HighImportance
} else {
button.importance = widget.MediumImportance
}
button.text = item.Text
button.textAlignment = fyne.TextAlignCenter
button.Refresh()
buttons.Objects = append(buttons.Objects, button)
}
return buttons
}
func (r *appTabsRenderer) updateIndicator(animate bool) {
if len(r.appTabs.Items) == 0 || r.appTabs.current < 0 {
r.indicator.Hide()
return
}
r.indicator.Show()
var selectedPos fyne.Position
var selectedSize fyne.Size
buttons := r.bar.Objects[0].(*fyne.Container).Objects
if r.appTabs.current >= len(buttons) {
if a := r.action; a != nil {
selectedPos = a.Position()
selectedSize = a.Size()
}
} else {
selected := buttons[r.appTabs.current]
selectedPos = selected.Position()
selectedSize = selected.Size()
}
var indicatorPos fyne.Position
var indicatorSize fyne.Size
th := r.appTabs.Theme()
pad := th.Size(theme.SizeNamePadding)
switch r.appTabs.location {
case TabLocationTop:
indicatorPos = fyne.NewPos(selectedPos.X, r.bar.MinSize().Height)
indicatorSize = fyne.NewSize(selectedSize.Width, pad)
case TabLocationLeading:
indicatorPos = fyne.NewPos(r.bar.MinSize().Width, selectedPos.Y)
indicatorSize = fyne.NewSize(pad, selectedSize.Height)
case TabLocationBottom:
indicatorPos = fyne.NewPos(selectedPos.X, r.bar.Position().Y-pad)
indicatorSize = fyne.NewSize(selectedSize.Width, pad)
case TabLocationTrailing:
indicatorPos = fyne.NewPos(r.bar.Position().X-pad, selectedPos.Y)
indicatorSize = fyne.NewSize(pad, selectedSize.Height)
}
r.moveIndicator(indicatorPos, indicatorSize, th, animate)
}
func (r *appTabsRenderer) updateTabs(max int) {
tabCount := len(r.appTabs.Items)
// Set overflow action
if tabCount <= max {
r.action.Hide()
r.bar.Layout = layout.NewStackLayout()
} else {
tabCount = max
r.action.Show()
// Set layout of tab bar containing tab buttons and overflow action
if r.appTabs.location == TabLocationLeading || r.appTabs.location == TabLocationTrailing {
r.bar.Layout = layout.NewBorderLayout(nil, r.action, nil, nil)
} else {
r.bar.Layout = layout.NewBorderLayout(nil, nil, nil, r.action)
}
}
buttons := r.buildTabButtons(tabCount)
r.bar.Objects = []fyne.CanvasObject{buttons}
if a := r.action; a != nil {
r.bar.Objects = append(r.bar.Objects, a)
}
r.bar.Refresh()
}