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

688 lines
20 KiB
Go

package widget
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/internal/cache"
"fyne.io/fyne/v2/theme"
)
// ScrollDirection represents the directions in which a Scroll can scroll its child content.
type ScrollDirection = fyne.ScrollDirection
// Constants for valid values of ScrollDirection.
const (
// ScrollBoth supports horizontal and vertical scrolling.
ScrollBoth ScrollDirection = iota
// ScrollHorizontalOnly specifies the scrolling should only happen left to right.
ScrollHorizontalOnly
// ScrollVerticalOnly specifies the scrolling should only happen top to bottom.
ScrollVerticalOnly
// ScrollNone turns off scrolling for this container.
//
// Since: 2.0
ScrollNone
)
type scrollBarOrientation int
// We default to vertical as 0 due to that being the original orientation offered
const (
scrollBarOrientationVertical scrollBarOrientation = 0
scrollBarOrientationHorizontal scrollBarOrientation = 1
scrollContainerMinSize = float32(32) // TODO consider the smallest useful scroll view?
// what fraction of the page to scroll when tapping on the scroll bar area
pageScrollFraction = float32(0.95)
)
type scrollBarRenderer struct {
BaseRenderer
scrollBar *scrollBar
background *canvas.Rectangle
minSize fyne.Size
}
func (r *scrollBarRenderer) Layout(size fyne.Size) {
r.background.Resize(size)
}
func (r *scrollBarRenderer) MinSize() fyne.Size {
return r.minSize
}
func (r *scrollBarRenderer) Refresh() {
th := theme.CurrentForWidget(r.scrollBar)
v := fyne.CurrentApp().Settings().ThemeVariant()
r.background.FillColor = th.Color(theme.ColorNameScrollBar, v)
r.background.CornerRadius = th.Size(theme.SizeNameScrollBarRadius)
r.background.Refresh()
}
var (
_ desktop.Hoverable = (*scrollBar)(nil)
_ fyne.Draggable = (*scrollBar)(nil)
)
type scrollBar struct {
Base
area *scrollBarArea
draggedDistance float32
dragStart float32
orientation scrollBarOrientation
}
func (b *scrollBar) CreateRenderer() fyne.WidgetRenderer {
th := theme.CurrentForWidget(b)
v := fyne.CurrentApp().Settings().ThemeVariant()
background := canvas.NewRectangle(th.Color(theme.ColorNameScrollBar, v))
background.CornerRadius = th.Size(theme.SizeNameScrollBarRadius)
r := &scrollBarRenderer{
scrollBar: b,
background: background,
}
r.SetObjects([]fyne.CanvasObject{background})
return r
}
func (b *scrollBar) Cursor() desktop.Cursor {
return desktop.DefaultCursor
}
func (b *scrollBar) DragEnd() {
b.area.isDragging = false
if fyne.CurrentDevice().IsMobile() {
b.area.MouseOut()
return
}
b.area.Refresh()
}
func (b *scrollBar) Dragged(e *fyne.DragEvent) {
if !b.area.isDragging {
b.area.isDragging = true
b.area.MouseIn(nil)
switch b.orientation {
case scrollBarOrientationHorizontal:
b.dragStart = b.Position().X
case scrollBarOrientationVertical:
b.dragStart = b.Position().Y
}
b.draggedDistance = 0
}
switch b.orientation {
case scrollBarOrientationHorizontal:
b.draggedDistance += e.Dragged.DX
case scrollBarOrientationVertical:
b.draggedDistance += e.Dragged.DY
}
b.area.moveBar(b.draggedDistance+b.dragStart, b.Size())
}
func (b *scrollBar) MouseIn(e *desktop.MouseEvent) {
b.area.MouseIn(e)
}
func (b *scrollBar) MouseMoved(*desktop.MouseEvent) {
}
func (b *scrollBar) MouseOut() {
b.area.MouseOut()
}
func newScrollBar(area *scrollBarArea) *scrollBar {
b := &scrollBar{area: area, orientation: area.orientation}
b.ExtendBaseWidget(b)
return b
}
func (a *scrollBarArea) isLarge() bool {
return a.isMouseIn || a.isDragging
}
type scrollBarAreaRenderer struct {
BaseRenderer
area *scrollBarArea
bar *scrollBar
background *canvas.Rectangle
}
func (r *scrollBarAreaRenderer) Layout(size fyne.Size) {
r.layoutWithTheme(theme.CurrentForWidget(r.area), size)
}
func (r *scrollBarAreaRenderer) layoutWithTheme(th fyne.Theme, size fyne.Size) {
var barHeight, barWidth, barX, barY float32
var bkgHeight, bkgWidth, bkgX, bkgY float32
switch r.area.orientation {
case scrollBarOrientationHorizontal:
barWidth, barHeight, barX, barY = r.barSizeAndOffset(th, r.area.scroll.Offset.X, r.area.scroll.Content.Size().Width, r.area.scroll.Size().Width)
r.area.barLeadingEdge = barX
r.area.barTrailingEdge = barX + barWidth
bkgWidth, bkgHeight, bkgX, bkgY = size.Width, barHeight, 0, barY
default:
barHeight, barWidth, barY, barX = r.barSizeAndOffset(th, r.area.scroll.Offset.Y, r.area.scroll.Content.Size().Height, r.area.scroll.Size().Height)
r.area.barLeadingEdge = barY
r.area.barTrailingEdge = barY + barHeight
bkgWidth, bkgHeight, bkgX, bkgY = barWidth, size.Height, barX, 0
}
r.bar.Move(fyne.NewPos(barX, barY))
r.bar.Resize(fyne.NewSize(barWidth, barHeight))
r.background.Move(fyne.NewPos(bkgX, bkgY))
r.background.Resize(fyne.NewSize(bkgWidth, bkgHeight))
}
func (r *scrollBarAreaRenderer) MinSize() fyne.Size {
th := theme.CurrentForWidget(r.area)
barSize := th.Size(theme.SizeNameScrollBar)
min := barSize
if !r.area.isLarge() {
min = th.Size(theme.SizeNameScrollBarSmall) * 2
}
switch r.area.orientation {
case scrollBarOrientationHorizontal:
return fyne.NewSize(barSize, min)
default:
return fyne.NewSize(min, barSize)
}
}
func (r *scrollBarAreaRenderer) Refresh() {
th := theme.CurrentForWidget(r.area)
r.bar.Refresh()
r.background.FillColor = th.Color(theme.ColorNameScrollBarBackground, fyne.CurrentApp().Settings().ThemeVariant())
r.background.Hidden = !r.area.isLarge()
r.layoutWithTheme(th, r.area.Size())
canvas.Refresh(r.bar)
canvas.Refresh(r.background)
}
func (r *scrollBarAreaRenderer) barSizeAndOffset(th fyne.Theme, contentOffset, contentLength, scrollLength float32) (length, width, lengthOffset, widthOffset float32) {
scrollBarSize := th.Size(theme.SizeNameScrollBar)
if scrollLength < contentLength {
portion := scrollLength / contentLength
length = float32(int(scrollLength)) * portion
length = fyne.Max(length, scrollBarSize)
} else {
length = scrollLength
}
if contentOffset != 0 {
lengthOffset = (scrollLength - length) * (contentOffset / (contentLength - scrollLength))
}
if r.area.isLarge() {
width = scrollBarSize
} else {
widthOffset = th.Size(theme.SizeNameScrollBarSmall)
width = widthOffset
}
return length, width, lengthOffset, widthOffset
}
var (
_ desktop.Hoverable = (*scrollBarArea)(nil)
_ fyne.Tappable = (*scrollBarArea)(nil)
)
type scrollBarArea struct {
Base
isDragging bool
isMouseIn bool
scroll *Scroll
bar *scrollBar
orientation scrollBarOrientation
// updated from renderer Layout
// coordinates Y in vertical orientation, X in horizontal
barLeadingEdge float32
barTrailingEdge float32
}
func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer {
th := theme.CurrentForWidget(a)
v := fyne.CurrentApp().Settings().ThemeVariant()
a.bar = newScrollBar(a)
background := canvas.NewRectangle(th.Color(theme.ColorNameScrollBarBackground, v))
background.Hidden = !a.isLarge()
return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{background, a.bar}), area: a, bar: a.bar, background: background}
}
func (a *scrollBarArea) Tapped(e *fyne.PointEvent) {
if isScrollerPageOnTap() {
a.scrollFullPageOnTap(e)
return
}
// scroll to tapped position
barSize := a.bar.Size()
switch a.orientation {
case scrollBarOrientationHorizontal:
if e.Position.X < a.barLeadingEdge || e.Position.X > a.barTrailingEdge {
a.moveBar(fyne.Max(0, e.Position.X-barSize.Width/2), barSize)
}
case scrollBarOrientationVertical:
if e.Position.Y < a.barLeadingEdge || e.Position.Y > a.barTrailingEdge {
a.moveBar(fyne.Max(0, e.Position.Y-barSize.Height/2), a.bar.Size())
}
}
}
func (a *scrollBarArea) scrollFullPageOnTap(e *fyne.PointEvent) {
// when tapping above/below or left/right of the bar, scroll the content
// nearly a full page (pageScrollFraction) up/down or left/right, respectively
newOffset := a.scroll.Offset
switch a.orientation {
case scrollBarOrientationHorizontal:
if e.Position.X < a.barLeadingEdge {
newOffset.X = fyne.Max(0, newOffset.X-a.scroll.Size().Width*pageScrollFraction)
} else if e.Position.X > a.barTrailingEdge {
viewWid := a.scroll.Size().Width
newOffset.X = fyne.Min(a.scroll.Content.Size().Width-viewWid, newOffset.X+viewWid*pageScrollFraction)
}
default:
if e.Position.Y < a.barLeadingEdge {
newOffset.Y = fyne.Max(0, newOffset.Y-a.scroll.Size().Height*pageScrollFraction)
} else if e.Position.Y > a.barTrailingEdge {
viewHt := a.scroll.Size().Height
newOffset.Y = fyne.Min(a.scroll.Content.Size().Height-viewHt, newOffset.Y+viewHt*pageScrollFraction)
}
}
if newOffset == a.scroll.Offset {
return
}
a.scroll.Offset = newOffset
if f := a.scroll.OnScrolled; f != nil {
f(a.scroll.Offset)
}
a.scroll.refreshWithoutOffsetUpdate()
}
func (a *scrollBarArea) MouseIn(*desktop.MouseEvent) {
a.isMouseIn = true
a.scroll.refreshBars()
}
func (a *scrollBarArea) MouseMoved(*desktop.MouseEvent) {
}
func (a *scrollBarArea) MouseOut() {
a.isMouseIn = false
if a.isDragging {
return
}
a.scroll.refreshBars()
}
func (a *scrollBarArea) moveBar(offset float32, barSize fyne.Size) {
oldX := a.scroll.Offset.X
oldY := a.scroll.Offset.Y
switch a.orientation {
case scrollBarOrientationHorizontal:
a.scroll.Offset.X = a.computeScrollOffset(barSize.Width, offset, a.scroll.Size().Width, a.scroll.Content.Size().Width)
default:
a.scroll.Offset.Y = a.computeScrollOffset(barSize.Height, offset, a.scroll.Size().Height, a.scroll.Content.Size().Height)
}
if f := a.scroll.OnScrolled; f != nil && (a.scroll.Offset.X != oldX || a.scroll.Offset.Y != oldY) {
f(a.scroll.Offset)
}
a.scroll.refreshWithoutOffsetUpdate()
}
func (a *scrollBarArea) computeScrollOffset(length, offset, scrollLength, contentLength float32) float32 {
maxOffset := scrollLength - length
if offset < 0 {
offset = 0
} else if offset > maxOffset {
offset = maxOffset
}
ratio := offset / maxOffset
scrollOffset := ratio * (contentLength - scrollLength)
return scrollOffset
}
func newScrollBarArea(scroll *Scroll, orientation scrollBarOrientation) *scrollBarArea {
a := &scrollBarArea{scroll: scroll, orientation: orientation}
a.ExtendBaseWidget(a)
return a
}
type scrollContainerRenderer struct {
BaseRenderer
scroll *Scroll
vertArea *scrollBarArea
horizArea *scrollBarArea
leftShadow, rightShadow *Shadow
topShadow, bottomShadow *Shadow
oldMinSize fyne.Size
}
func (r *scrollContainerRenderer) layoutBars(size fyne.Size) {
scrollerSize := r.scroll.Size()
if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth {
r.vertArea.Resize(fyne.NewSize(r.vertArea.MinSize().Width, size.Height))
r.vertArea.Move(fyne.NewPos(scrollerSize.Width-r.vertArea.Size().Width, 0))
r.topShadow.Resize(fyne.NewSize(size.Width, 0))
r.bottomShadow.Resize(fyne.NewSize(size.Width, 0))
r.bottomShadow.Move(fyne.NewPos(0, scrollerSize.Height))
}
if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth {
r.horizArea.Resize(fyne.NewSize(size.Width, r.horizArea.MinSize().Height))
r.horizArea.Move(fyne.NewPos(0, scrollerSize.Height-r.horizArea.Size().Height))
r.leftShadow.Resize(fyne.NewSize(0, size.Height))
r.rightShadow.Resize(fyne.NewSize(0, size.Height))
r.rightShadow.Move(fyne.NewPos(scrollerSize.Width, 0))
}
r.updatePosition()
}
func (r *scrollContainerRenderer) Layout(size fyne.Size) {
c := r.scroll.Content
c.Resize(c.MinSize().Max(size))
r.layoutBars(size)
}
func (r *scrollContainerRenderer) MinSize() fyne.Size {
return r.scroll.MinSize()
}
func (r *scrollContainerRenderer) Refresh() {
r.horizArea.Refresh()
r.vertArea.Refresh()
r.leftShadow.Refresh()
r.topShadow.Refresh()
r.rightShadow.Refresh()
r.bottomShadow.Refresh()
if len(r.BaseRenderer.Objects()) == 0 || r.BaseRenderer.Objects()[0] != r.scroll.Content {
// push updated content object to baseRenderer
r.BaseRenderer.Objects()[0] = r.scroll.Content
}
size := r.scroll.Size()
newMin := r.scroll.Content.MinSize()
if r.oldMinSize == newMin && r.oldMinSize == r.scroll.Content.Size() &&
(size.Width <= r.oldMinSize.Width && size.Height <= r.oldMinSize.Height) {
r.layoutBars(size)
return
}
r.oldMinSize = newMin
r.Layout(size)
}
func (r *scrollContainerRenderer) handleAreaVisibility(contentSize, scrollSize float32, area *scrollBarArea) {
if contentSize <= scrollSize {
area.Hide()
} else if r.scroll.Visible() {
area.Show()
}
}
func (r *scrollContainerRenderer) handleShadowVisibility(offset, contentSize, scrollSize float32, shadowStart fyne.CanvasObject, shadowEnd fyne.CanvasObject) {
if !r.scroll.Visible() {
return
}
if offset > 0 {
shadowStart.Show()
} else {
shadowStart.Hide()
}
if offset < contentSize-scrollSize {
shadowEnd.Show()
} else {
shadowEnd.Hide()
}
}
func (r *scrollContainerRenderer) updatePosition() {
if r.scroll.Content == nil {
return
}
scrollSize := r.scroll.Size()
contentSize := r.scroll.Content.Size()
r.scroll.Content.Move(fyne.NewPos(-r.scroll.Offset.X, -r.scroll.Offset.Y))
if r.scroll.Direction == ScrollVerticalOnly || r.scroll.Direction == ScrollBoth {
r.handleAreaVisibility(contentSize.Height, scrollSize.Height, r.vertArea)
r.handleShadowVisibility(r.scroll.Offset.Y, contentSize.Height, scrollSize.Height, r.topShadow, r.bottomShadow)
cache.Renderer(r.vertArea).Layout(scrollSize)
} else {
r.vertArea.Hide()
r.topShadow.Hide()
r.bottomShadow.Hide()
}
if r.scroll.Direction == ScrollHorizontalOnly || r.scroll.Direction == ScrollBoth {
r.handleAreaVisibility(contentSize.Width, scrollSize.Width, r.horizArea)
r.handleShadowVisibility(r.scroll.Offset.X, contentSize.Width, scrollSize.Width, r.leftShadow, r.rightShadow)
cache.Renderer(r.horizArea).Layout(scrollSize)
} else {
r.horizArea.Hide()
r.leftShadow.Hide()
r.rightShadow.Hide()
}
if r.scroll.Direction != ScrollHorizontalOnly {
canvas.Refresh(r.vertArea) // this is required to force the canvas to update, we have no "Redraw()"
} else {
canvas.Refresh(r.horizArea) // this is required like above but if we are horizontal
}
}
// Scroll defines a container that is smaller than the Content.
// The Offset is used to determine the position of the child widgets within the container.
type Scroll struct {
Base
minSize fyne.Size
Direction ScrollDirection
Content fyne.CanvasObject
Offset fyne.Position
// OnScrolled can be set to be notified when the Scroll has changed position.
// You should not update the Scroll.Offset from this method.
//
// Since: 2.0
OnScrolled func(fyne.Position) `json:"-"`
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (s *Scroll) CreateRenderer() fyne.WidgetRenderer {
scr := &scrollContainerRenderer{
BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{s.Content}),
scroll: s,
}
scr.vertArea = newScrollBarArea(s, scrollBarOrientationVertical)
scr.topShadow = NewShadow(ShadowBottom, SubmergedContentLevel)
scr.bottomShadow = NewShadow(ShadowTop, SubmergedContentLevel)
scr.horizArea = newScrollBarArea(s, scrollBarOrientationHorizontal)
scr.leftShadow = NewShadow(ShadowRight, SubmergedContentLevel)
scr.rightShadow = NewShadow(ShadowLeft, SubmergedContentLevel)
scr.SetObjects(append(scr.Objects(), scr.topShadow, scr.bottomShadow, scr.leftShadow, scr.rightShadow,
scr.vertArea, scr.horizArea))
scr.updatePosition()
return scr
}
// ScrollToBottom will scroll content to container bottom - to show latest info which end user just added
func (s *Scroll) ScrollToBottom() {
s.scrollBy(0, -1*(s.Content.MinSize().Height-s.Size().Height-s.Offset.Y))
s.refreshBars()
}
// ScrollToTop will scroll content to container top
func (s *Scroll) ScrollToTop() {
s.ScrollToOffset(fyne.Position{})
s.refreshBars()
}
// DragEnd will stop scrolling on mobile has stopped
func (s *Scroll) DragEnd() {
}
// Dragged will scroll on any drag - bar or otherwise - for mobile
func (s *Scroll) Dragged(e *fyne.DragEvent) {
if !fyne.CurrentDevice().IsMobile() {
return
}
if s.updateOffset(e.Dragged.DX, e.Dragged.DY) {
s.refreshWithoutOffsetUpdate()
}
}
// MinSize returns the smallest size this widget can shrink to
func (s *Scroll) MinSize() fyne.Size {
min := fyne.NewSize(scrollContainerMinSize, scrollContainerMinSize).Max(s.minSize)
switch s.Direction {
case ScrollHorizontalOnly:
min.Height = fyne.Max(min.Height, s.Content.MinSize().Height)
case ScrollVerticalOnly:
min.Width = fyne.Max(min.Width, s.Content.MinSize().Width)
case ScrollNone:
return s.Content.MinSize()
}
return min
}
// SetMinSize specifies a minimum size for this scroll container.
// If the specified size is larger than the content size then scrolling will not be enabled
// This can be helpful to appear larger than default if the layout is collapsing this widget.
func (s *Scroll) SetMinSize(size fyne.Size) {
s.minSize = size
}
// Refresh causes this widget to be redrawn in it's current state
func (s *Scroll) Refresh() {
s.refreshBars()
if s.Content != nil {
s.Content.Refresh()
}
}
// Resize is called when this scroller should change size. We refresh to ensure the scroll bars are updated.
func (s *Scroll) Resize(sz fyne.Size) {
if sz == s.Size() {
return
}
s.Base.Resize(sz)
s.refreshBars()
}
// ScrollToOffset will update the location of the content of this scroll container.
//
// Since: 2.6
func (s *Scroll) ScrollToOffset(p fyne.Position) {
if s.Offset == p {
return
}
s.Offset = p
s.refreshBars()
}
func (s *Scroll) refreshWithoutOffsetUpdate() {
s.Base.Refresh()
}
// Scrolled is called when an input device triggers a scroll event
func (s *Scroll) Scrolled(ev *fyne.ScrollEvent) {
if s.Direction != ScrollNone {
s.scrollBy(ev.Scrolled.DX, ev.Scrolled.DY)
}
}
func (s *Scroll) refreshBars() {
s.updateOffset(0, 0)
s.refreshWithoutOffsetUpdate()
}
func (s *Scroll) scrollBy(dx, dy float32) {
min := s.Content.MinSize()
size := s.Size()
if size.Width < min.Width && size.Height >= min.Height && dx == 0 {
dx, dy = dy, dx
}
if s.updateOffset(dx, dy) {
s.refreshWithoutOffsetUpdate()
}
}
func (s *Scroll) updateOffset(deltaX, deltaY float32) bool {
size := s.Size()
contentSize := s.Content.Size()
if contentSize.Width <= size.Width && contentSize.Height <= size.Height {
if s.Offset.X != 0 || s.Offset.Y != 0 {
s.Offset.X = 0
s.Offset.Y = 0
return true
}
return false
}
oldX := s.Offset.X
oldY := s.Offset.Y
min := s.Content.MinSize()
s.Offset.X = computeOffset(s.Offset.X, -deltaX, size.Width, min.Width)
s.Offset.Y = computeOffset(s.Offset.Y, -deltaY, size.Height, min.Height)
moved := s.Offset.X != oldX || s.Offset.Y != oldY
if f := s.OnScrolled; f != nil && moved {
f(s.Offset)
}
return moved
}
func computeOffset(start, delta, outerWidth, innerWidth float32) float32 {
offset := start + delta
if offset+outerWidth >= innerWidth {
offset = innerWidth - outerWidth
}
return fyne.Max(offset, 0)
}
// NewScroll creates a scrollable parent wrapping the specified content.
// Note that this may cause the MinSize to be smaller than that of the passed object.
func NewScroll(content fyne.CanvasObject) *Scroll {
s := newScrollContainerWithDirection(ScrollBoth, content)
s.ExtendBaseWidget(s)
return s
}
// NewHScroll create a scrollable parent wrapping the specified content.
// Note that this may cause the MinSize.Width to be smaller than that of the passed object.
func NewHScroll(content fyne.CanvasObject) *Scroll {
s := newScrollContainerWithDirection(ScrollHorizontalOnly, content)
s.ExtendBaseWidget(s)
return s
}
// NewVScroll create a scrollable parent wrapping the specified content.
// Note that this may cause the MinSize.Height to be smaller than that of the passed object.
func NewVScroll(content fyne.CanvasObject) *Scroll {
s := newScrollContainerWithDirection(ScrollVerticalOnly, content)
s.ExtendBaseWidget(s)
return s
}
func newScrollContainerWithDirection(direction ScrollDirection, content fyne.CanvasObject) *Scroll {
s := &Scroll{
Direction: direction,
Content: content,
}
s.ExtendBaseWidget(s)
return s
}