VideoTools/vendor/fyne.io/fyne/v2/widget/button.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

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)
})
}