fckeuspy-go/vendor/fyne.io/fyne/v2/internal/widget/scroller.go

684 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)
var _ 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
}
var _ desktop.Hoverable = (*scrollBarArea)(nil)
var _ 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 false /*todo - read MacOS system setting for scroll by page*/ {
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.Subtract(p).IsZero() {
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
}