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

462 lines
11 KiB
Go

package widget
import (
"fmt"
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
)
const defaultPlaceHolder string = "(Select one)"
var (
_ fyne.Widget = (*Select)(nil)
_ desktop.Hoverable = (*Select)(nil)
_ fyne.Tappable = (*Select)(nil)
_ fyne.Focusable = (*Select)(nil)
_ fyne.Disableable = (*Select)(nil)
)
// Select widget has a list of options, with the current one shown, and triggers an event func when clicked
type Select struct {
DisableableWidget
// Alignment sets the text alignment of the select and its list of options.
//
// Since: 2.1
Alignment fyne.TextAlign
Selected string
Options []string
PlaceHolder string
OnChanged func(string) `json:"-"`
binder basicBinder
focused bool
hovered bool
popUp *PopUpMenu
tapAnim *fyne.Animation
}
// NewSelect creates a new select widget with the set list of options and changes handler
func NewSelect(options []string, changed func(string)) *Select {
s := &Select{
OnChanged: changed,
Options: options,
PlaceHolder: defaultPlaceHolder,
}
s.ExtendBaseWidget(s)
return s
}
// NewSelectWithData returns a new select widget connected to the specified data source.
//
// Since: 2.6
func NewSelectWithData(options []string, data binding.String) *Select {
sel := NewSelect(options, nil)
sel.Bind(data)
return sel
}
// Bind connects the specified data source to this select.
// The current value will be displayed and any changes in the data will cause the widget
// to update.
//
// Since: 2.6
func (s *Select) Bind(data binding.String) {
s.binder.SetCallback(s.updateFromData)
s.binder.Bind(data)
s.OnChanged = func(_ string) {
s.binder.CallWithData(s.writeData)
}
}
// ClearSelected clears the current option of the select widget. After
// clearing the current option, the Select widget's PlaceHolder will
// be displayed.
func (s *Select) ClearSelected() {
s.updateSelected("")
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (s *Select) CreateRenderer() fyne.WidgetRenderer {
s.ExtendBaseWidget(s)
th := s.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
icon := NewIcon(th.Icon(theme.IconNameArrowDropDown))
if s.PlaceHolder == "" {
s.PlaceHolder = defaultPlaceHolder
}
txtProv := NewRichTextWithText(s.Selected)
txtProv.inset = fyne.NewSquareSize(th.Size(theme.SizeNamePadding))
txtProv.ExtendBaseWidget(txtProv)
txtProv.Truncation = fyne.TextTruncateEllipsis
if s.Disabled() {
txtProv.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled
}
background := &canvas.Rectangle{}
tapBG := canvas.NewRectangle(color.Transparent)
s.tapAnim = newButtonTapAnimation(tapBG, s, th)
s.tapAnim.Curve = fyne.AnimationEaseOut
objects := []fyne.CanvasObject{background, tapBG, txtProv, icon}
r := &selectRenderer{icon, txtProv, background, objects, s}
background.FillColor = r.bgColor(th, v)
background.CornerRadius = th.Size(theme.SizeNameInputRadius)
r.updateIcon(th)
r.updateLabel()
return r
}
// FocusGained is called after this Select has gained focus.
func (s *Select) FocusGained() {
s.focused = true
s.Refresh()
}
// FocusLost is called after this Select has lost focus.
func (s *Select) FocusLost() {
s.focused = false
s.Refresh()
}
// Hide hides the select.
func (s *Select) Hide() {
if s.popUp != nil {
s.popUp.Hide()
s.popUp = nil
}
s.BaseWidget.Hide()
}
// MinSize returns the size that this widget should not shrink below
func (s *Select) MinSize() fyne.Size {
s.ExtendBaseWidget(s)
return s.BaseWidget.MinSize()
}
// MouseIn is called when a desktop pointer enters the widget
func (s *Select) MouseIn(*desktop.MouseEvent) {
s.hovered = true
s.Refresh()
}
// MouseMoved is called when a desktop pointer hovers over the widget
func (s *Select) MouseMoved(*desktop.MouseEvent) {
}
// MouseOut is called when a desktop pointer exits the widget
func (s *Select) MouseOut() {
s.hovered = false
s.Refresh()
}
// Move changes the relative position of the select.
func (s *Select) Move(pos fyne.Position) {
s.BaseWidget.Move(pos)
if s.popUp != nil {
s.popUp.Move(s.popUpPos())
}
}
// Resize sets a new size for a widget.
// Note this should not be used if the widget is being managed by a Layout within a Container.
func (s *Select) Resize(size fyne.Size) {
s.BaseWidget.Resize(size)
if s.popUp != nil {
s.popUp.Resize(fyne.NewSize(size.Width, s.popUp.MinSize().Height))
}
}
// SelectedIndex returns the index value of the currently selected item in Options list.
// It will return -1 if there is no selection.
func (s *Select) SelectedIndex() int {
for i, option := range s.Options {
if s.Selected == option {
return i
}
}
return -1 // not selected/found
}
// SetOptions updates the list of options available and refreshes the widget
//
// Since: 2.4
func (s *Select) SetOptions(options []string) {
s.Options = options
s.Refresh()
}
// SetSelected sets the current option of the select widget
func (s *Select) SetSelected(text string) {
for _, option := range s.Options {
if text == option {
s.updateSelected(text)
}
}
}
// SetSelectedIndex will set the Selected option from the value in Options list at index position.
func (s *Select) SetSelectedIndex(index int) {
if index < 0 || index >= len(s.Options) {
return
}
s.updateSelected(s.Options[index])
}
// Tapped is called when a pointer tapped event is captured and triggers any tap handler
func (s *Select) Tapped(*fyne.PointEvent) {
if s.Disabled() {
return
}
if !s.focused {
focusIfNotMobile(s.super())
}
s.tapAnimation()
s.Refresh()
s.showPopUp()
}
// TypedKey is called if a key event happens while this Select is focused.
func (s *Select) TypedKey(event *fyne.KeyEvent) {
switch event.Name {
case fyne.KeySpace, fyne.KeyUp, fyne.KeyDown:
s.showPopUp()
case fyne.KeyRight:
i := s.SelectedIndex() + 1
if i >= len(s.Options) {
i = 0
}
s.SetSelectedIndex(i)
case fyne.KeyLeft:
i := s.SelectedIndex() - 1
if i < 0 {
i = len(s.Options) - 1
}
s.SetSelectedIndex(i)
}
}
// TypedRune is called if a text event happens while this Select is focused.
func (s *Select) TypedRune(_ rune) {
// intentionally left blank
}
// Unbind disconnects any configured data source from this Select.
// The current value will remain at the last value of the data source.
//
// Since: 2.6
func (s *Select) Unbind() {
s.OnChanged = nil
s.binder.Unbind()
}
func (s *Select) popUpPos() fyne.Position {
buttonPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(s.super())
return buttonPos.Add(fyne.NewPos(0, s.Size().Height-s.Theme().Size(theme.SizeNameInputBorder)))
}
func (s *Select) showPopUp() {
items := make([]*fyne.MenuItem, len(s.Options))
for i := range s.Options {
text := s.Options[i] // capture
items[i] = fyne.NewMenuItem(text, func() {
s.updateSelected(text)
s.popUp = nil
})
}
c := fyne.CurrentApp().Driver().CanvasForObject(s.super())
pop := NewPopUpMenu(fyne.NewMenu("", items...), c)
pop.alignment = s.Alignment
pop.ShowAtPosition(s.popUpPos())
pop.Resize(fyne.NewSize(s.Size().Width, pop.MinSize().Height))
pop.OnDismiss = func() {
pop.Hide()
if s.popUp == pop {
s.popUp = nil
}
}
s.popUp = pop
}
func (s *Select) tapAnimation() {
if s.tapAnim == nil {
return
}
s.tapAnim.Stop()
if fyne.CurrentApp().Settings().ShowAnimations() {
s.tapAnim.Start()
}
}
func (s *Select) updateFromData(data binding.DataItem) {
if data == nil {
return
}
stringSource, ok := data.(binding.String)
if !ok {
return
}
val, err := stringSource.Get()
if err != nil {
return
}
s.SetSelected(val)
}
func (s *Select) updateSelected(text string) {
s.Selected = text
if s.OnChanged != nil {
s.OnChanged(s.Selected)
}
s.Refresh()
}
func (s *Select) writeData(data binding.DataItem) {
if data == nil {
return
}
stringTarget, ok := data.(binding.String)
if !ok {
return
}
currentValue, err := stringTarget.Get()
if err != nil {
return
}
if currentValue != s.Selected {
err := stringTarget.Set(s.Selected)
if err != nil {
fyne.LogError(fmt.Sprintf("Failed to set binding value to %s", s.Selected), err)
}
}
}
type selectRenderer struct {
icon *Icon
label *RichText
background *canvas.Rectangle
objects []fyne.CanvasObject
combo *Select
}
func (s *selectRenderer) Objects() []fyne.CanvasObject {
return s.objects
}
func (s *selectRenderer) Destroy() {}
// Layout the components of the button widget
func (s *selectRenderer) Layout(size fyne.Size) {
th := s.combo.Theme()
pad := th.Size(theme.SizeNamePadding)
iconSize := th.Size(theme.SizeNameInlineIcon)
innerPad := th.Size(theme.SizeNameInnerPadding)
s.background.Resize(fyne.NewSize(size.Width, size.Height))
s.label.inset = fyne.NewSquareSize(pad)
iconPos := fyne.NewPos(size.Width-iconSize-innerPad, (size.Height-iconSize)/2)
labelSize := fyne.NewSize(iconPos.X-pad, s.label.MinSize().Height)
s.label.Resize(labelSize)
s.label.Move(fyne.NewPos(pad, (size.Height-labelSize.Height)/2))
s.icon.Resize(fyne.NewSquareSize(iconSize))
s.icon.Move(iconPos)
}
// MinSize calculates the minimum size of a select button.
// This is based on the selected text, the drop icon and a standard amount of padding added.
func (s *selectRenderer) MinSize() fyne.Size {
th := s.combo.Theme()
innerPad := th.Size(theme.SizeNameInnerPadding)
minPlaceholderWidth := fyne.MeasureText(s.combo.PlaceHolder, th.Size(theme.SizeNameText), fyne.TextStyle{}).Width
min := s.label.MinSize()
min.Width = minPlaceholderWidth
min = min.Add(fyne.NewSize(innerPad*3, innerPad))
return min.Add(fyne.NewSize(th.Size(theme.SizeNameInlineIcon)+innerPad, 0))
}
func (s *selectRenderer) Refresh() {
th := s.combo.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
s.updateLabel()
s.updateIcon(th)
s.background.FillColor = s.bgColor(th, v)
s.background.CornerRadius = s.combo.Theme().Size(theme.SizeNameInputRadius)
s.Layout(s.combo.Size())
if s.combo.popUp != nil {
s.combo.popUp.alignment = s.combo.Alignment
s.combo.popUp.Move(s.combo.popUpPos())
s.combo.popUp.Resize(fyne.NewSize(s.combo.Size().Width, s.combo.popUp.MinSize().Height))
s.combo.popUp.Refresh()
}
s.background.Refresh()
canvas.Refresh(s.combo.super())
}
func (s *selectRenderer) bgColor(th fyne.Theme, v fyne.ThemeVariant) color.Color {
if s.combo.Disabled() {
return th.Color(theme.ColorNameDisabledButton, v)
}
if s.combo.focused {
return th.Color(theme.ColorNameFocus, v)
}
if s.combo.hovered {
return th.Color(theme.ColorNameHover, v)
}
return th.Color(theme.ColorNameInputBackground, v)
}
func (s *selectRenderer) updateIcon(th fyne.Theme) {
icon := th.Icon(theme.IconNameArrowDropDown)
if s.combo.Disabled() {
s.icon.Resource = theme.NewDisabledResource(icon)
} else {
s.icon.Resource = icon
}
s.icon.Refresh()
}
func (s *selectRenderer) updateLabel() {
if s.combo.PlaceHolder == "" {
s.combo.PlaceHolder = defaultPlaceHolder
}
segment := s.label.Segments[0].(*TextSegment)
segment.Style.Alignment = s.combo.Alignment
if s.combo.Disabled() {
segment.Style.ColorName = theme.ColorNameDisabled
} else {
segment.Style.ColorName = theme.ColorNameForeground
}
if s.combo.Selected == "" {
segment.Text = s.combo.PlaceHolder
} else {
segment.Text = s.combo.Selected
}
s.label.Refresh()
}