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

399 lines
10 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/internal/widget"
"fyne.io/fyne/v2/theme"
)
// Check widget has a text label and a checked (or unchecked) icon and triggers an event func when toggled
type Check struct {
DisableableWidget
Text string
Checked bool
// Partial check is when there is an indeterminate state (usually meaning that child items are some-what checked).
// Turning this on will override the checked state and show a dash icon (neither checked nor unchecked).
// The user interaction cannot turn this on, tapping a partial check state will set `Checked` to true.
//
// Since: 2.6
Partial bool
OnChanged func(bool) `json:"-"`
focused bool
hovered bool
binder basicBinder
minSize fyne.Size // cached for hover/tap position calculations
}
// NewCheck creates a new check widget with the set label and change handler
func NewCheck(label string, changed func(bool)) *Check {
c := &Check{
Text: label,
OnChanged: changed,
}
c.ExtendBaseWidget(c)
return c
}
// NewCheckWithData returns a check widget connected with the specified data source.
//
// Since: 2.0
func NewCheckWithData(label string, data binding.Bool) *Check {
check := NewCheck(label, nil)
check.Bind(data)
return check
}
// Bind connects the specified data source to this Check.
// The current value will be displayed and any changes in the data will cause the widget to update.
// User interactions with this Check will set the value into the data source.
//
// Since: 2.0
func (c *Check) Bind(data binding.Bool) {
c.binder.SetCallback(c.updateFromData)
c.binder.Bind(data)
c.OnChanged = func(_ bool) {
c.binder.CallWithData(c.writeData)
}
}
// SetChecked sets the checked state and refreshes widget
// If the `Partial` state is set this will be turned off to respect the `checked` bool passed in here.
func (c *Check) SetChecked(checked bool) {
if checked == c.Checked && !c.Partial {
return
}
c.Partial = false
c.Checked = checked
onChanged := c.OnChanged
if onChanged != nil {
onChanged(checked)
}
c.Refresh()
}
// Hide this widget, if it was previously visible
func (c *Check) Hide() {
if c.focused {
c.FocusLost()
impl := c.super()
if c := fyne.CurrentApp().Driver().CanvasForObject(impl); c != nil {
c.Focus(nil)
}
}
c.BaseWidget.Hide()
}
// MouseIn is called when a desktop pointer enters the widget
func (c *Check) MouseIn(me *desktop.MouseEvent) {
c.MouseMoved(me)
}
// MouseOut is called when a desktop pointer exits the widget
func (c *Check) MouseOut() {
if c.hovered {
c.hovered = false
c.Refresh()
}
}
// MouseMoved is called when a desktop pointer hovers over the widget
func (c *Check) MouseMoved(me *desktop.MouseEvent) {
if c.Disabled() {
return
}
oldHovered := c.hovered
// only hovered if cached minSize has not been initialized (test code)
// or the pointer is within the "active" area of the widget (its minSize)
c.hovered = c.minSize.IsZero() ||
(me.Position.X <= c.minSize.Width && me.Position.Y <= c.minSize.Height)
if oldHovered != c.hovered {
c.Refresh()
}
}
// Tapped is called when a pointer tapped event is captured and triggers any change handler
func (c *Check) Tapped(pe *fyne.PointEvent) {
if c.Disabled() {
return
}
minHeight := c.minSize.Height
minY := (c.Size().Height - minHeight) / 2
if !c.minSize.IsZero() &&
(pe.Position.X > c.minSize.Width || pe.Position.Y < minY || pe.Position.Y > minY+minHeight) {
// tapped outside the active area of the widget
return
}
if !c.focused {
focusIfNotMobile(c.super())
}
c.SetChecked(!c.Checked)
}
// MinSize returns the size that this widget should not shrink below
func (c *Check) MinSize() fyne.Size {
c.ExtendBaseWidget(c)
c.minSize = c.BaseWidget.MinSize()
return c.minSize
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (c *Check) CreateRenderer() fyne.WidgetRenderer {
th := c.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
c.ExtendBaseWidget(c)
bg := canvas.NewImageFromResource(th.Icon(theme.IconNameCheckButtonFill))
icon := canvas.NewImageFromResource(th.Icon(theme.IconNameCheckButton))
text := canvas.NewText(c.Text, th.Color(theme.ColorNameForeground, v))
text.Alignment = fyne.TextAlignLeading
focusIndicator := canvas.NewCircle(th.Color(theme.ColorNameBackground, v))
r := &checkRenderer{
widget.NewBaseRenderer([]fyne.CanvasObject{focusIndicator, bg, icon, text}),
bg,
icon,
text,
focusIndicator,
c,
}
r.applyTheme(th, v)
r.updateLabel()
r.updateResource(th)
r.updateFocusIndicator(th, v)
return r
}
// FocusGained is called when the Check has been given focus.
func (c *Check) FocusGained() {
if c.Disabled() {
return
}
c.focused = true
c.Refresh()
}
// FocusLost is called when the Check has had focus removed.
func (c *Check) FocusLost() {
c.focused = false
c.Refresh()
}
// TypedRune receives text input events when the Check is focused.
func (c *Check) TypedRune(r rune) {
if c.Disabled() {
return
}
if r == ' ' {
c.SetChecked(!c.Checked)
}
}
// TypedKey receives key input events when the Check is focused.
func (c *Check) TypedKey(key *fyne.KeyEvent) {}
// SetText sets the text of the Check
//
// Since: 2.4
func (c *Check) SetText(text string) {
c.Text = text
c.Refresh()
}
// Unbind disconnects any configured data source from this Check.
// The current value will remain at the last value of the data source.
//
// Since: 2.0
func (c *Check) Unbind() {
c.OnChanged = nil
c.binder.Unbind()
}
func (c *Check) updateFromData(data binding.DataItem) {
if data == nil {
return
}
boolSource, ok := data.(binding.Bool)
if !ok {
return
}
val, err := boolSource.Get()
if err != nil {
fyne.LogError("Error getting current data value", err)
return
}
c.SetChecked(val) // if val != c.Checked, this will call updateFromData again, but only once
}
func (c *Check) writeData(data binding.DataItem) {
if data == nil {
return
}
boolTarget, ok := data.(binding.Bool)
if !ok {
return
}
currentValue, err := boolTarget.Get()
if err != nil {
return
}
if currentValue != c.Checked {
err := boolTarget.Set(c.Checked)
if err != nil {
fyne.LogError(fmt.Sprintf("Failed to set binding value to %t", c.Checked), err)
}
}
}
type checkRenderer struct {
widget.BaseRenderer
bg, icon *canvas.Image
label *canvas.Text
focusIndicator *canvas.Circle
check *Check
}
// MinSize calculates the minimum size of a check.
// This is based on the contained text, the check icon and a standard amount of padding added.
func (c *checkRenderer) MinSize() fyne.Size {
th := c.check.Theme()
pad4 := th.Size(theme.SizeNameInnerPadding) * 2
min := c.label.MinSize().Add(fyne.NewSize(th.Size(theme.SizeNameInlineIcon)+pad4, pad4))
if c.check.Text != "" {
min.Add(fyne.NewSize(th.Size(theme.SizeNamePadding), 0))
}
return min
}
// Layout the components of the check widget
func (c *checkRenderer) Layout(size fyne.Size) {
th := c.check.Theme()
innerPadding := th.Size(theme.SizeNameInnerPadding)
borderSize := th.Size(theme.SizeNameInputBorder)
iconInlineSize := th.Size(theme.SizeNameInlineIcon)
focusIndicatorSize := fyne.NewSquareSize(iconInlineSize + innerPadding)
c.focusIndicator.Resize(focusIndicatorSize)
c.focusIndicator.Move(fyne.NewPos(borderSize, (size.Height-focusIndicatorSize.Height)/2))
xOff := focusIndicatorSize.Width + borderSize*2
labelSize := size.SubtractWidthHeight(xOff, 0)
c.label.Resize(labelSize)
c.label.Move(fyne.NewPos(xOff, 0))
iconPos := fyne.NewPos(innerPadding/2+borderSize, (size.Height-iconInlineSize)/2)
iconSize := fyne.NewSquareSize(iconInlineSize)
c.bg.Move(iconPos)
c.bg.Resize(iconSize)
c.icon.Move(iconPos)
c.icon.Resize(iconSize)
}
// applyTheme updates this Check to the current theme
func (c *checkRenderer) applyTheme(th fyne.Theme, v fyne.ThemeVariant) {
c.label.Color = th.Color(theme.ColorNameForeground, v)
c.label.TextSize = th.Size(theme.SizeNameText)
if c.check.Disabled() {
c.label.Color = th.Color(theme.ColorNameDisabled, v)
}
}
func (c *checkRenderer) Refresh() {
th := c.check.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
c.applyTheme(th, v)
c.updateLabel()
c.updateResource(th)
c.updateFocusIndicator(th, v)
canvas.Refresh(c.check.super())
}
// must be called while holding c.check.propertyLock for reading
func (c *checkRenderer) updateLabel() {
c.label.Text = c.check.Text
}
// must be called while holding c.check.propertyLock for reading
func (c *checkRenderer) updateResource(th fyne.Theme) {
res := theme.NewThemedResource(th.Icon(theme.IconNameCheckButton))
res.ColorName = theme.ColorNameInputBorder
bgRes := theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonFill))
bgRes.ColorName = theme.ColorNameInputBackground
if c.check.Partial {
res = theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonPartial))
res.ColorName = theme.ColorNamePrimary
bgRes.ColorName = theme.ColorNameBackground
} else if c.check.Checked {
res = theme.NewThemedResource(th.Icon(theme.IconNameCheckButtonChecked))
res.ColorName = theme.ColorNamePrimary
bgRes.ColorName = theme.ColorNameBackground
}
if c.check.Disabled() {
if c.check.Checked {
res = theme.NewThemedResource(theme.CheckButtonCheckedIcon())
}
res.ColorName = theme.ColorNameDisabled
bgRes.ColorName = theme.ColorNameBackground
}
c.icon.Resource = res
c.icon.Refresh()
c.bg.Resource = bgRes
c.bg.Refresh()
}
// must be called while holding c.check.propertyLock for reading
func (c *checkRenderer) updateFocusIndicator(th fyne.Theme, v fyne.ThemeVariant) {
if c.check.Disabled() {
c.focusIndicator.FillColor = color.Transparent
} else if c.check.focused {
c.focusIndicator.FillColor = th.Color(theme.ColorNameFocus, v)
} else if c.check.hovered {
c.focusIndicator.FillColor = th.Color(theme.ColorNameHover, v)
} else {
c.focusIndicator.FillColor = color.Transparent
}
}
func focusIfNotMobile(w fyne.Widget) {
if w == nil {
return
}
if !fyne.CurrentDevice().IsMobile() {
if c := fyne.CurrentApp().Driver().CanvasForObject(w); c != nil {
c.Focus(w.(fyne.Focusable))
}
}
}