VideoTools/vendor/fyne.io/fyne/v2/container/innerwindow.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

445 lines
11 KiB
Go

package container
import (
"image/color"
"runtime"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
intWidget "fyne.io/fyne/v2/internal/widget"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type titleBarButtonMode int
const (
modeClose titleBarButtonMode = iota
modeMinimize
modeMaximize
modeIcon
)
var _ fyne.Widget = (*InnerWindow)(nil)
// InnerWindow defines a container that wraps content in a window border - that can then be placed inside
// a regular container/canvas.
//
// Since: 2.5
type InnerWindow struct {
widget.BaseWidget
CloseIntercept func() `json:"-"`
OnDragged, OnResized func(*fyne.DragEvent) `json:"-"`
OnMinimized, OnMaximized, OnTappedBar, OnTappedIcon func() `json:"-"`
Icon fyne.Resource
// Alignment allows an inner window to specify if the buttons should be on the left
// (`ButtonAlignLeading`) or right of the window border.
//
// Since: 2.6
Alignment widget.ButtonAlign
title string
content *fyne.Container
maximized bool
}
// NewInnerWindow creates a new window border around the given `content`, displaying the `title` along the top.
// This will behave like a normal contain and will probably want to be added to a `MultipleWindows` parent.
//
// Since: 2.5
func NewInnerWindow(title string, content fyne.CanvasObject) *InnerWindow {
w := &InnerWindow{title: title, content: NewPadded(content)}
w.ExtendBaseWidget(w)
return w
}
func (w *InnerWindow) Close() {
w.Hide()
}
func (w *InnerWindow) CreateRenderer() fyne.WidgetRenderer {
w.ExtendBaseWidget(w)
th := w.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
min := newBorderButton(theme.WindowMinimizeIcon(), modeMinimize, th, w.OnMinimized)
if w.OnMinimized == nil {
min.Disable()
}
max := newBorderButton(theme.WindowMaximizeIcon(), modeMaximize, th, w.OnMaximized)
if w.OnMaximized == nil {
max.Disable()
}
close := newBorderButton(theme.WindowCloseIcon(), modeClose, th, func() {
if f := w.CloseIntercept; f != nil {
f()
} else {
w.Close()
}
})
buttons := NewCenter(NewHBox(close, min, max))
borderIcon := newBorderButton(w.Icon, modeIcon, th, func() {
if f := w.OnTappedIcon; f != nil {
f()
}
})
if w.OnTappedIcon == nil {
borderIcon.Disable()
}
if w.Icon == nil {
borderIcon.Hide()
}
title := newDraggableLabel(w.title, w)
title.Truncation = fyne.TextTruncateEllipsis
height := w.Theme().Size(theme.SizeNameWindowTitleBarHeight)
off := (height - title.labelMinSize().Height) / 2
barMid := New(layout.NewCustomPaddedLayout(off, 0, 0, 0), title)
if w.buttonPosition() == widget.ButtonAlignTrailing {
buttons = NewCenter(NewHBox(min, max, close))
}
bg := canvas.NewRectangle(th.Color(theme.ColorNameOverlayBackground, v))
contentBG := canvas.NewRectangle(th.Color(theme.ColorNameBackground, v))
corner := newDraggableCorner(w)
bar := New(&titleBarLayout{buttons: buttons, icon: borderIcon, title: barMid, win: w},
buttons, borderIcon, barMid)
if w.content == nil {
w.content = NewPadded(canvas.NewRectangle(color.Transparent))
}
objects := []fyne.CanvasObject{bg, contentBG, bar, w.content, corner}
r := &innerWindowRenderer{
ShadowingRenderer: intWidget.NewShadowingRenderer(objects, intWidget.DialogLevel),
win: w, bar: bar, buttonBox: buttons, buttons: []*borderButton{close, min, max}, bg: bg,
corner: corner, contentBG: contentBG, icon: borderIcon,
}
r.Layout(w.Size())
return r
}
func (w *InnerWindow) SetContent(obj fyne.CanvasObject) {
w.content.Objects[0] = obj
w.content.Refresh()
}
// SetMaximized tells the window if the maximized state should be set or not.
//
// Since: 2.6
func (w *InnerWindow) SetMaximized(max bool) {
w.maximized = max
w.Refresh()
}
func (w *InnerWindow) SetPadded(pad bool) {
if pad {
w.content.Layout = layout.NewPaddedLayout()
} else {
w.content.Layout = layout.NewStackLayout()
}
w.content.Refresh()
}
func (w *InnerWindow) SetTitle(title string) {
w.title = title
w.Refresh()
}
func (w *InnerWindow) buttonPosition() widget.ButtonAlign {
if w.Alignment != widget.ButtonAlignCenter {
return w.Alignment
}
if runtime.GOOS == "windows" || runtime.GOOS == "linux" || strings.Contains(runtime.GOOS, "bsd") {
return widget.ButtonAlignTrailing
}
// macOS
return widget.ButtonAlignLeading
}
var _ fyne.WidgetRenderer = (*innerWindowRenderer)(nil)
type innerWindowRenderer struct {
*intWidget.ShadowingRenderer
win *InnerWindow
bar, buttonBox *fyne.Container
buttons []*borderButton
icon *borderButton
bg, contentBG *canvas.Rectangle
corner fyne.CanvasObject
}
func (i *innerWindowRenderer) Layout(size fyne.Size) {
th := i.win.Theme()
pad := th.Size(theme.SizeNamePadding)
i.LayoutShadow(size, fyne.Position{})
i.bg.Resize(size)
barHeight := i.win.Theme().Size(theme.SizeNameWindowTitleBarHeight)
i.bar.Move(fyne.NewPos(pad, 0))
i.bar.Resize(fyne.NewSize(size.Width-pad*2, barHeight))
innerPos := fyne.NewPos(pad, barHeight)
innerSize := fyne.NewSize(size.Width-pad*2, size.Height-pad-barHeight)
i.contentBG.Move(innerPos)
i.contentBG.Resize(innerSize)
i.win.content.Move(innerPos)
i.win.content.Resize(innerSize)
cornerSize := i.corner.MinSize()
i.corner.Move(fyne.NewPos(size.Components()).Subtract(cornerSize).AddXY(1, 1))
i.corner.Resize(cornerSize)
}
func (i *innerWindowRenderer) MinSize() fyne.Size {
th := i.win.Theme()
pad := th.Size(theme.SizeNamePadding)
contentMin := i.win.content.MinSize()
barHeight := th.Size(theme.SizeNameWindowTitleBarHeight)
innerWidth := fyne.Max(i.bar.MinSize().Width, contentMin.Width)
return fyne.NewSize(innerWidth+pad*2, contentMin.Height+pad+barHeight)
}
func (i *innerWindowRenderer) Refresh() {
th := i.win.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
i.bg.FillColor = th.Color(theme.ColorNameOverlayBackground, v)
i.bg.Refresh()
i.contentBG.FillColor = th.Color(theme.ColorNameBackground, v)
i.contentBG.Refresh()
if i.win.buttonPosition() == widget.ButtonAlignTrailing {
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[1], i.buttons[2], i.buttons[0]}
} else {
i.buttonBox.Objects[0].(*fyne.Container).Objects = []fyne.CanvasObject{i.buttons[0], i.buttons[1], i.buttons[2]}
}
for _, b := range i.buttons {
b.setTheme(th)
}
i.bar.Refresh()
if i.win.OnMinimized == nil {
i.buttons[1].Disable()
} else {
i.buttons[1].SetOnTapped(i.win.OnMinimized)
i.buttons[1].Enable()
}
max := i.buttons[2]
if i.win.OnMaximized == nil {
i.buttons[2].Disable()
} else {
max.SetOnTapped(i.win.OnMaximized)
max.Enable()
}
if i.win.maximized {
max.b.SetIcon(theme.ViewRestoreIcon())
} else {
max.b.SetIcon(theme.WindowMaximizeIcon())
}
title := i.bar.Objects[2].(*fyne.Container).Objects[0].(*draggableLabel)
title.SetText(i.win.title)
i.ShadowingRenderer.RefreshShadow()
if i.win.OnTappedIcon == nil {
i.icon.Disable()
} else {
i.icon.Enable()
}
if i.win.Icon != nil {
i.icon.b.SetIcon(i.win.Icon)
i.icon.Show()
} else {
i.icon.Hide()
}
}
type draggableLabel struct {
widget.Label
win *InnerWindow
}
func newDraggableLabel(title string, win *InnerWindow) *draggableLabel {
d := &draggableLabel{win: win}
d.ExtendBaseWidget(d)
d.Text = title
return d
}
func (d *draggableLabel) Dragged(ev *fyne.DragEvent) {
if f := d.win.OnDragged; f != nil {
f(ev)
}
}
func (d *draggableLabel) DragEnd() {
}
func (d *draggableLabel) MinSize() fyne.Size {
width := d.Label.MinSize().Width
height := d.Label.Theme().Size(theme.SizeNameWindowButtonHeight)
return fyne.NewSize(width, height)
}
func (d *draggableLabel) Tapped(_ *fyne.PointEvent) {
if f := d.win.OnTappedBar; f != nil {
f()
}
}
func (d *draggableLabel) labelMinSize() fyne.Size {
return d.Label.MinSize()
}
type draggableCorner struct {
widget.BaseWidget
win *InnerWindow
}
func newDraggableCorner(w *InnerWindow) *draggableCorner {
d := &draggableCorner{win: w}
d.ExtendBaseWidget(d)
return d
}
func (c *draggableCorner) CreateRenderer() fyne.WidgetRenderer {
prop := canvas.NewImageFromResource(fyne.CurrentApp().Settings().Theme().Icon(theme.IconNameDragCornerIndicator))
prop.SetMinSize(fyne.NewSquareSize(16))
return widget.NewSimpleRenderer(prop)
}
func (c *draggableCorner) Dragged(ev *fyne.DragEvent) {
if f := c.win.OnResized; f != nil {
c.win.OnResized(ev)
}
}
func (c *draggableCorner) DragEnd() {
}
type borderButton struct {
widget.BaseWidget
b *widget.Button
c *ThemeOverride
mode titleBarButtonMode
}
func newBorderButton(icon fyne.Resource, mode titleBarButtonMode, th fyne.Theme, fn func()) *borderButton {
buttonImportance := widget.MediumImportance
if mode == modeIcon {
buttonImportance = widget.LowImportance
}
b := &widget.Button{Icon: icon, Importance: buttonImportance, OnTapped: fn}
c := NewThemeOverride(b, &buttonTheme{Theme: th, mode: mode})
ret := &borderButton{b: b, c: c, mode: mode}
ret.ExtendBaseWidget(ret)
return ret
}
func (b *borderButton) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(b.c)
}
func (b *borderButton) Disable() {
b.b.Disable()
}
func (b *borderButton) Enable() {
b.b.Enable()
}
func (b *borderButton) SetOnTapped(fn func()) {
b.b.OnTapped = fn
}
func (b *borderButton) MinSize() fyne.Size {
height := b.Theme().Size(theme.SizeNameWindowButtonHeight)
return fyne.NewSquareSize(height)
}
func (b *borderButton) setTheme(th fyne.Theme) {
b.c.Theme = &buttonTheme{Theme: th, mode: b.mode}
}
type buttonTheme struct {
fyne.Theme
mode titleBarButtonMode
}
func (b *buttonTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
switch n {
case theme.ColorNameHover:
if b.mode == modeClose {
n = theme.ColorNameError
}
}
return b.Theme.Color(n, v)
}
func (b *buttonTheme) Size(n fyne.ThemeSizeName) float32 {
switch n {
case theme.SizeNameInputRadius:
if b.mode == modeIcon {
return 0
}
n = theme.SizeNameWindowButtonRadius
case theme.SizeNameInlineIcon:
n = theme.SizeNameWindowButtonIcon
}
return b.Theme.Size(n)
}
type titleBarLayout struct {
win *InnerWindow
buttons, icon, title fyne.CanvasObject
}
func (t *titleBarLayout) Layout(_ []fyne.CanvasObject, s fyne.Size) {
buttonMinWidth := t.buttons.MinSize().Width
t.buttons.Resize(fyne.NewSize(buttonMinWidth, s.Height))
t.icon.Resize(fyne.NewSquareSize(s.Height))
usedWidth := buttonMinWidth
if t.icon.Visible() {
usedWidth += s.Height
}
t.title.Resize(fyne.NewSize(s.Width-usedWidth, s.Height))
if t.win.buttonPosition() == widget.ButtonAlignTrailing {
t.buttons.Move(fyne.NewPos(s.Width-buttonMinWidth, 0))
t.icon.Move(fyne.Position{})
if t.icon.Visible() {
t.title.Move(fyne.NewPos(s.Height, 0))
} else {
t.title.Move(fyne.Position{})
}
} else {
t.buttons.Move(fyne.NewPos(0, 0))
t.icon.Move(fyne.NewPos(s.Width-s.Height, 0))
t.title.Move(fyne.NewPos(buttonMinWidth, 0))
}
}
func (t *titleBarLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
buttonMin := t.buttons.MinSize()
iconMin := t.icon.MinSize()
titleMin := t.title.MinSize() // can truncate
return fyne.NewSize(buttonMin.Width+iconMin.Width+titleMin.Width,
fyne.Max(fyne.Max(buttonMin.Height, iconMin.Height), titleMin.Height))
}