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
456 lines
12 KiB
Go
456 lines
12 KiB
Go
package widget
|
|
|
|
import (
|
|
"image/color"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/driver/desktop"
|
|
col "fyne.io/fyne/v2/internal/color"
|
|
"fyne.io/fyne/v2/internal/svg"
|
|
"fyne.io/fyne/v2/internal/widget"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
)
|
|
|
|
// ButtonAlign represents the horizontal alignment of a button.
|
|
type ButtonAlign int
|
|
|
|
// ButtonIconPlacement represents the ordering of icon & text within a button.
|
|
type ButtonIconPlacement int
|
|
|
|
// ButtonImportance represents how prominent the button should appear
|
|
//
|
|
// Since: 1.4
|
|
//
|
|
// Deprecated: Use widget.Importance instead
|
|
type ButtonImportance = Importance
|
|
|
|
// ButtonStyle determines the behaviour and rendering of a button.
|
|
type ButtonStyle int
|
|
|
|
const (
|
|
// ButtonAlignCenter aligns the icon and the text centrally.
|
|
ButtonAlignCenter ButtonAlign = iota
|
|
// ButtonAlignLeading aligns the icon and the text with the leading edge.
|
|
ButtonAlignLeading
|
|
// ButtonAlignTrailing aligns the icon and the text with the trailing edge.
|
|
ButtonAlignTrailing
|
|
)
|
|
|
|
const (
|
|
// ButtonIconLeadingText aligns the icon on the leading edge of the text.
|
|
ButtonIconLeadingText ButtonIconPlacement = iota
|
|
// ButtonIconTrailingText aligns the icon on the trailing edge of the text.
|
|
ButtonIconTrailingText
|
|
)
|
|
|
|
var _ fyne.Focusable = (*Button)(nil)
|
|
|
|
// Button widget has a text label and triggers an event func when clicked
|
|
type Button struct {
|
|
DisableableWidget
|
|
Text string
|
|
Icon fyne.Resource
|
|
// Specify how prominent the button should be, High will highlight the button and Low will remove some decoration.
|
|
//
|
|
// Since: 1.4
|
|
Importance Importance
|
|
Alignment ButtonAlign
|
|
IconPlacement ButtonIconPlacement
|
|
|
|
OnTapped func() `json:"-"`
|
|
|
|
hovered, focused bool
|
|
tapAnim *fyne.Animation
|
|
}
|
|
|
|
// NewButton creates a new button widget with the set label and tap handler
|
|
func NewButton(label string, tapped func()) *Button {
|
|
button := &Button{
|
|
Text: label,
|
|
OnTapped: tapped,
|
|
}
|
|
|
|
button.ExtendBaseWidget(button)
|
|
return button
|
|
}
|
|
|
|
// NewButtonWithIcon creates a new button widget with the specified label, themed icon and tap handler
|
|
func NewButtonWithIcon(label string, icon fyne.Resource, tapped func()) *Button {
|
|
button := &Button{
|
|
Text: label,
|
|
Icon: icon,
|
|
OnTapped: tapped,
|
|
}
|
|
|
|
button.ExtendBaseWidget(button)
|
|
return button
|
|
}
|
|
|
|
// CreateRenderer is a private method to Fyne which links this widget to its renderer
|
|
func (b *Button) CreateRenderer() fyne.WidgetRenderer {
|
|
b.ExtendBaseWidget(b)
|
|
th := b.Theme()
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
|
|
seg := &TextSegment{Text: b.Text, Style: RichTextStyleStrong}
|
|
seg.Style.Alignment = fyne.TextAlignCenter
|
|
text := NewRichText(seg)
|
|
text.inset = fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding))
|
|
|
|
background := canvas.NewRectangle(th.Color(theme.ColorNameButton, v))
|
|
background.CornerRadius = th.Size(theme.SizeNameInputRadius)
|
|
tapBG := canvas.NewRectangle(color.Transparent)
|
|
b.tapAnim = newButtonTapAnimation(tapBG, b, th)
|
|
b.tapAnim.Curve = fyne.AnimationEaseOut
|
|
objects := []fyne.CanvasObject{
|
|
background,
|
|
tapBG,
|
|
text,
|
|
}
|
|
r := &buttonRenderer{
|
|
BaseRenderer: widget.NewBaseRenderer(objects),
|
|
background: background,
|
|
tapBG: tapBG,
|
|
button: b,
|
|
label: text,
|
|
layout: layout.NewHBoxLayout(),
|
|
}
|
|
r.updateIconAndText()
|
|
r.applyTheme()
|
|
return r
|
|
}
|
|
|
|
// Cursor returns the cursor type of this widget
|
|
func (b *Button) Cursor() desktop.Cursor {
|
|
return desktop.DefaultCursor
|
|
}
|
|
|
|
// FocusGained is a hook called by the focus handling logic after this object gained the focus.
|
|
func (b *Button) FocusGained() {
|
|
b.focused = true
|
|
b.Refresh()
|
|
}
|
|
|
|
// FocusLost is a hook called by the focus handling logic after this object lost the focus.
|
|
func (b *Button) FocusLost() {
|
|
b.focused = false
|
|
b.Refresh()
|
|
}
|
|
|
|
// MinSize returns the size that this widget should not shrink below
|
|
func (b *Button) MinSize() fyne.Size {
|
|
b.ExtendBaseWidget(b)
|
|
return b.BaseWidget.MinSize()
|
|
}
|
|
|
|
// MouseIn is called when a desktop pointer enters the widget
|
|
func (b *Button) MouseIn(*desktop.MouseEvent) {
|
|
b.hovered = true
|
|
b.Refresh()
|
|
}
|
|
|
|
// MouseMoved is called when a desktop pointer hovers over the widget
|
|
func (b *Button) MouseMoved(*desktop.MouseEvent) {
|
|
}
|
|
|
|
// MouseOut is called when a desktop pointer exits the widget
|
|
func (b *Button) MouseOut() {
|
|
b.hovered = false
|
|
b.Refresh()
|
|
}
|
|
|
|
// SetIcon updates the icon on a label - pass nil to hide an icon
|
|
func (b *Button) SetIcon(icon fyne.Resource) {
|
|
b.Icon = icon
|
|
|
|
b.Refresh()
|
|
}
|
|
|
|
// SetText allows the button label to be changed
|
|
func (b *Button) SetText(text string) {
|
|
b.Text = text
|
|
|
|
b.Refresh()
|
|
}
|
|
|
|
// Tapped is called when a pointer tapped event is captured and triggers any tap handler
|
|
func (b *Button) Tapped(*fyne.PointEvent) {
|
|
if b.Disabled() {
|
|
return
|
|
}
|
|
|
|
b.tapAnimation()
|
|
|
|
if onTapped := b.OnTapped; onTapped != nil {
|
|
onTapped()
|
|
}
|
|
}
|
|
|
|
// TypedRune is a hook called by the input handling logic on text input events if this object is focused.
|
|
func (b *Button) TypedRune(rune) {
|
|
}
|
|
|
|
// TypedKey is a hook called by the input handling logic on key events if this object is focused.
|
|
func (b *Button) TypedKey(ev *fyne.KeyEvent) {
|
|
if ev.Name == fyne.KeySpace {
|
|
b.Tapped(nil)
|
|
}
|
|
}
|
|
|
|
func (b *Button) tapAnimation() {
|
|
if b.tapAnim == nil {
|
|
return
|
|
}
|
|
b.tapAnim.Stop()
|
|
|
|
if fyne.CurrentApp().Settings().ShowAnimations() {
|
|
b.tapAnim.Start()
|
|
}
|
|
}
|
|
|
|
type buttonRenderer struct {
|
|
widget.BaseRenderer
|
|
|
|
icon *canvas.Image
|
|
label *RichText
|
|
background *canvas.Rectangle
|
|
tapBG *canvas.Rectangle
|
|
button *Button
|
|
layout fyne.Layout
|
|
}
|
|
|
|
// Layout the components of the button widget
|
|
func (r *buttonRenderer) Layout(size fyne.Size) {
|
|
r.background.Resize(size)
|
|
r.tapBG.Resize(size)
|
|
|
|
th := r.button.Theme()
|
|
padding := r.padding(th)
|
|
hasIcon := r.icon != nil
|
|
hasLabel := r.label.Segments[0].(*TextSegment).Text != ""
|
|
if !hasIcon && !hasLabel {
|
|
// Nothing to layout
|
|
return
|
|
}
|
|
iconSize := fyne.NewSquareSize(th.Size(theme.SizeNameInlineIcon))
|
|
labelSize := r.label.MinSize()
|
|
|
|
if hasLabel {
|
|
if hasIcon {
|
|
// Both
|
|
var objects []fyne.CanvasObject
|
|
if r.button.IconPlacement == ButtonIconLeadingText {
|
|
objects = append(objects, r.icon, r.label)
|
|
} else {
|
|
objects = append(objects, r.label, r.icon)
|
|
}
|
|
r.icon.SetMinSize(iconSize)
|
|
min := r.layout.MinSize(objects)
|
|
r.layout.Layout(objects, min)
|
|
pos := alignedPosition(r.button.Alignment, padding, min, size)
|
|
labelOff := (min.Height - labelSize.Height) / 2
|
|
r.label.Move(r.label.Position().Add(pos).AddXY(0, labelOff))
|
|
r.icon.Move(r.icon.Position().Add(pos))
|
|
} else {
|
|
// Label Only
|
|
r.label.Move(alignedPosition(r.button.Alignment, padding, labelSize, size))
|
|
r.label.Resize(labelSize)
|
|
}
|
|
} else {
|
|
// Icon Only
|
|
r.icon.Move(alignedPosition(r.button.Alignment, padding, iconSize, size))
|
|
r.icon.Resize(iconSize)
|
|
}
|
|
}
|
|
|
|
// MinSize calculates the minimum size of a button.
|
|
// This is based on the contained text, any icon that is set and a standard
|
|
// amount of padding added.
|
|
func (r *buttonRenderer) MinSize() (size fyne.Size) {
|
|
th := r.button.Theme()
|
|
hasIcon := r.icon != nil
|
|
hasLabel := r.label.Segments[0].(*TextSegment).Text != ""
|
|
iconSize := fyne.NewSquareSize(th.Size(theme.SizeNameInlineIcon))
|
|
labelSize := r.label.MinSize()
|
|
if hasLabel {
|
|
size.Width = labelSize.Width
|
|
}
|
|
if hasIcon {
|
|
if hasLabel {
|
|
size.Width += th.Size(theme.SizeNamePadding)
|
|
}
|
|
size.Width += iconSize.Width
|
|
}
|
|
size.Height = fyne.Max(labelSize.Height, iconSize.Height)
|
|
size = size.Add(r.padding(th))
|
|
return size
|
|
}
|
|
|
|
func (r *buttonRenderer) Refresh() {
|
|
th := r.button.Theme()
|
|
r.label.inset = fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding))
|
|
|
|
r.label.Segments[0].(*TextSegment).Text = r.button.Text
|
|
r.updateIconAndText()
|
|
r.applyTheme()
|
|
|
|
r.background.Refresh()
|
|
r.Layout(r.button.Size())
|
|
canvas.Refresh(r.button.super())
|
|
}
|
|
|
|
// applyTheme updates this button to match the current theme
|
|
// must be called with the button propertyLock RLocked
|
|
func (r *buttonRenderer) applyTheme() {
|
|
th := r.button.Theme()
|
|
fgColorName, bgColorName, bgBlendName := r.buttonColorNames()
|
|
if bg := r.background; bg != nil {
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
bgColor := color.Color(color.Transparent)
|
|
if bgColorName != "" {
|
|
bgColor = th.Color(bgColorName, v)
|
|
}
|
|
if bgBlendName != "" {
|
|
bgColor = blendColor(bgColor, th.Color(bgBlendName, v))
|
|
}
|
|
bg.FillColor = bgColor
|
|
bg.CornerRadius = th.Size(theme.SizeNameInputRadius)
|
|
bg.Refresh()
|
|
}
|
|
|
|
r.label.Segments[0].(*TextSegment).Style.ColorName = fgColorName
|
|
r.label.Refresh()
|
|
if r.icon != nil && r.icon.Resource != nil {
|
|
icon := r.icon.Resource
|
|
if r.button.Importance != MediumImportance && r.button.Importance != LowImportance {
|
|
if thRes, ok := icon.(fyne.ThemedResource); ok {
|
|
if thRes.ThemeColorName() != fgColorName {
|
|
icon = theme.NewColoredResource(icon, fgColorName)
|
|
}
|
|
}
|
|
}
|
|
r.icon.Resource = icon
|
|
r.icon.Refresh()
|
|
}
|
|
}
|
|
|
|
func (r *buttonRenderer) buttonColorNames() (foreground, background, backgroundBlend fyne.ThemeColorName) {
|
|
foreground = theme.ColorNameForeground
|
|
b := r.button
|
|
if b.Disabled() {
|
|
foreground = theme.ColorNameDisabled
|
|
if b.Importance != LowImportance {
|
|
background = theme.ColorNameDisabledButton
|
|
}
|
|
} else if b.focused {
|
|
backgroundBlend = theme.ColorNameFocus
|
|
} else if b.hovered {
|
|
backgroundBlend = theme.ColorNameHover
|
|
}
|
|
if background == "" {
|
|
switch b.Importance {
|
|
case DangerImportance:
|
|
foreground = theme.ColorNameForegroundOnError
|
|
background = theme.ColorNameError
|
|
case HighImportance:
|
|
foreground = theme.ColorNameForegroundOnPrimary
|
|
background = theme.ColorNamePrimary
|
|
case LowImportance:
|
|
if backgroundBlend != "" {
|
|
background = theme.ColorNameButton
|
|
}
|
|
case SuccessImportance:
|
|
foreground = theme.ColorNameForegroundOnSuccess
|
|
background = theme.ColorNameSuccess
|
|
case WarningImportance:
|
|
foreground = theme.ColorNameForegroundOnWarning
|
|
background = theme.ColorNameWarning
|
|
default:
|
|
background = theme.ColorNameButton
|
|
}
|
|
}
|
|
return foreground, background, backgroundBlend
|
|
}
|
|
|
|
func (r *buttonRenderer) padding(th fyne.Theme) fyne.Size {
|
|
return fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding) * 2)
|
|
}
|
|
|
|
// must be called with r.button.propertyLock RLocked
|
|
func (r *buttonRenderer) updateIconAndText() {
|
|
if r.button.Icon != nil && !r.button.Hidden {
|
|
icon := r.button.Icon
|
|
if r.icon == nil {
|
|
r.icon = canvas.NewImageFromResource(icon)
|
|
r.icon.FillMode = canvas.ImageFillContain
|
|
r.SetObjects([]fyne.CanvasObject{r.background, r.tapBG, r.label, r.icon})
|
|
}
|
|
// TODO support disabling bitmap resource not just SVG
|
|
if r.button.Disabled() && svg.IsResourceSVG(icon) {
|
|
icon = theme.NewDisabledResource(icon)
|
|
}
|
|
r.icon.Resource = icon
|
|
r.icon.Refresh()
|
|
r.icon.Show()
|
|
} else if r.icon != nil {
|
|
r.icon.Hide()
|
|
}
|
|
if r.button.Text == "" {
|
|
r.label.Hide()
|
|
} else {
|
|
r.label.Show()
|
|
}
|
|
r.label.Refresh()
|
|
}
|
|
|
|
func alignedPosition(align ButtonAlign, padding, objectSize, layoutSize fyne.Size) (pos fyne.Position) {
|
|
pos.Y = (layoutSize.Height - objectSize.Height) / 2
|
|
switch align {
|
|
case ButtonAlignCenter:
|
|
pos.X = (layoutSize.Width - objectSize.Width) / 2
|
|
case ButtonAlignLeading:
|
|
pos.X = padding.Width / 2
|
|
case ButtonAlignTrailing:
|
|
pos.X = layoutSize.Width - objectSize.Width - padding.Width/2
|
|
}
|
|
return pos
|
|
}
|
|
|
|
func blendColor(under, over color.Color) color.Color {
|
|
// This alpha blends with the over operator, and accounts for RGBA() returning alpha-premultiplied values
|
|
dstR, dstG, dstB, dstA := under.RGBA()
|
|
srcR, srcG, srcB, srcA := over.RGBA()
|
|
|
|
srcAlpha := float32(srcA) / 0xFFFF
|
|
dstAlpha := float32(dstA) / 0xFFFF
|
|
|
|
outAlpha := srcAlpha + dstAlpha*(1-srcAlpha)
|
|
outR := srcR + uint32(float32(dstR)*(1-srcAlpha))
|
|
outG := srcG + uint32(float32(dstG)*(1-srcAlpha))
|
|
outB := srcB + uint32(float32(dstB)*(1-srcAlpha))
|
|
// We create an RGBA64 here because the color components are already alpha-premultiplied 16-bit values (they're just stored in uint32s).
|
|
return color.RGBA64{R: uint16(outR), G: uint16(outG), B: uint16(outB), A: uint16(outAlpha * 0xFFFF)}
|
|
}
|
|
|
|
func newButtonTapAnimation(bg *canvas.Rectangle, w fyne.Widget, th fyne.Theme) *fyne.Animation {
|
|
v := fyne.CurrentApp().Settings().ThemeVariant()
|
|
return fyne.NewAnimation(canvas.DurationStandard, func(done float32) {
|
|
mid := w.Size().Width / 2
|
|
size := mid * done
|
|
bg.Resize(fyne.NewSize(size*2, w.Size().Height))
|
|
bg.Move(fyne.NewPos(mid-size, 0))
|
|
|
|
r, g, bb, a := col.ToNRGBA(th.Color(theme.ColorNamePressed, v))
|
|
aa := uint8(a)
|
|
fade := aa - uint8(float32(aa)*done)
|
|
if fade > 0 {
|
|
bg.FillColor = &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(bb), A: fade}
|
|
} else {
|
|
bg.FillColor = color.Transparent
|
|
}
|
|
canvas.Refresh(bg)
|
|
})
|
|
}
|