| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535 |
- 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 int
- // 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?
- )
- 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() {
- r.background.FillColor = theme.ScrollBarColor()
- r.background.Refresh()
- }
- var _ desktop.Hoverable = (*scrollBar)(nil)
- var _ fyne.Draggable = (*scrollBar)(nil)
- type scrollBar struct {
- Base
- area *scrollBarArea
- draggedDistance float32
- dragStart float32
- isDragged bool
- orientation scrollBarOrientation
- }
- func (b *scrollBar) CreateRenderer() fyne.WidgetRenderer {
- background := canvas.NewRectangle(theme.ScrollBarColor())
- 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.isDragged = false
- }
- func (b *scrollBar) Dragged(e *fyne.DragEvent) {
- if !b.isDragged {
- b.isDragged = true
- 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
- }
- type scrollBarAreaRenderer struct {
- BaseRenderer
- area *scrollBarArea
- bar *scrollBar
- }
- func (r *scrollBarAreaRenderer) Layout(_ fyne.Size) {
- var barHeight, barWidth, barX, barY float32
- switch r.area.orientation {
- case scrollBarOrientationHorizontal:
- barWidth, barHeight, barX, barY = r.barSizeAndOffset(r.area.scroll.Offset.X, r.area.scroll.Content.Size().Width, r.area.scroll.Size().Width)
- default:
- barHeight, barWidth, barY, barX = r.barSizeAndOffset(r.area.scroll.Offset.Y, r.area.scroll.Content.Size().Height, r.area.scroll.Size().Height)
- }
- r.bar.Move(fyne.NewPos(barX, barY))
- r.bar.Resize(fyne.NewSize(barWidth, barHeight))
- }
- func (r *scrollBarAreaRenderer) MinSize() fyne.Size {
- min := theme.ScrollBarSize()
- if !r.area.isLarge {
- min = theme.ScrollBarSmallSize() * 2
- }
- switch r.area.orientation {
- case scrollBarOrientationHorizontal:
- return fyne.NewSize(theme.ScrollBarSize(), min)
- default:
- return fyne.NewSize(min, theme.ScrollBarSize())
- }
- }
- func (r *scrollBarAreaRenderer) Refresh() {
- r.Layout(r.area.Size())
- canvas.Refresh(r.bar)
- }
- func (r *scrollBarAreaRenderer) barSizeAndOffset(contentOffset, contentLength, scrollLength float32) (length, width, lengthOffset, widthOffset float32) {
- if scrollLength < contentLength {
- portion := scrollLength / contentLength
- length = float32(int(scrollLength)) * portion
- if length < theme.ScrollBarSize() {
- length = theme.ScrollBarSize()
- }
- } else {
- length = scrollLength
- }
- if contentOffset != 0 {
- lengthOffset = (scrollLength - length) * (contentOffset / (contentLength - scrollLength))
- }
- if r.area.isLarge {
- width = theme.ScrollBarSize()
- } else {
- widthOffset = theme.ScrollBarSmallSize()
- width = theme.ScrollBarSmallSize()
- }
- return
- }
- var _ desktop.Hoverable = (*scrollBarArea)(nil)
- type scrollBarArea struct {
- Base
- isLarge bool
- scroll *Scroll
- orientation scrollBarOrientation
- }
- func (a *scrollBarArea) CreateRenderer() fyne.WidgetRenderer {
- bar := newScrollBar(a)
- return &scrollBarAreaRenderer{BaseRenderer: NewBaseRenderer([]fyne.CanvasObject{bar}), area: a, bar: bar}
- }
- func (a *scrollBarArea) MouseIn(*desktop.MouseEvent) {
- a.isLarge = true
- a.scroll.Refresh()
- }
- func (a *scrollBarArea) MouseMoved(*desktop.MouseEvent) {
- }
- func (a *scrollBarArea) MouseOut() {
- a.isLarge = false
- a.scroll.Refresh()
- }
- 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) {
- 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(r.scroll.Size().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, r.scroll.size.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, r.scroll.Size().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(r.scroll.size.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() {
- 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
- }
- if r.oldMinSize == r.scroll.Content.MinSize() && r.oldMinSize == r.scroll.Content.Size() &&
- (r.scroll.Size().Width <= r.oldMinSize.Width && r.scroll.Size().Height <= r.oldMinSize.Height) {
- r.layoutBars(r.scroll.Size())
- return
- }
- r.oldMinSize = r.scroll.Content.MinSize()
- r.Layout(r.scroll.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(r.scroll.size)
- } 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(r.scroll.size)
- } 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)
- }
- // 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.Refresh()
- }
- // ScrollToTop will scroll content to container top
- func (s *Scroll) ScrollToTop() {
- s.scrollBy(0, -s.Offset.Y)
- }
- // 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.updateOffset(0, 0)
- s.refreshWithoutOffsetUpdate()
- }
- // 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.Refresh()
- }
- 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) {
- s.scrollBy(ev.Scrolled.DX, ev.Scrolled.DY)
- }
- func (s *Scroll) scrollBy(dx, dy float32) {
- if s.Size().Width < s.Content.MinSize().Width && s.Size().Height >= s.Content.MinSize().Height && dx == 0 {
- dx, dy = dy, dx
- }
- if s.updateOffset(dx, dy) {
- s.refreshWithoutOffsetUpdate()
- }
- }
- func (s *Scroll) updateOffset(deltaX, deltaY float32) bool {
- if s.Content.Size().Width <= s.Size().Width && s.Content.Size().Height <= s.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
- s.Offset.X = computeOffset(s.Offset.X, -deltaX, s.Size().Width, s.Content.MinSize().Width)
- s.Offset.Y = computeOffset(s.Offset.Y, -deltaY, s.Size().Height, s.Content.MinSize().Height)
- if f := s.OnScrolled; f != nil && (s.Offset.X != oldX || s.Offset.Y != oldY) {
- f(s.Offset)
- }
- return true
- }
- func computeOffset(start, delta, outerWidth, innerWidth float32) float32 {
- offset := start + delta
- if offset+outerWidth >= innerWidth {
- offset = innerWidth - outerWidth
- }
- if offset < 0 {
- offset = 0
- }
- return offset
- }
- // 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
- }
|