Skip to content

Commit

Permalink
Merge pull request #5407 from dweymouth/collections-thread-update
Browse files Browse the repository at this point in the history
List and GridWrap threading simplifications
  • Loading branch information
dweymouth authored Jan 16, 2025
2 parents 1b5d475 + 257988f commit 817fbd8
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 126 deletions.
142 changes: 64 additions & 78 deletions widget/gridwrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,26 @@ type GridWrapItemID = int
type GridWrap struct {
BaseWidget

Length func() int `json:"-"`
CreateItem func() fyne.CanvasObject `json:"-"`
UpdateItem func(id GridWrapItemID, item fyne.CanvasObject) `json:"-"`
OnSelected func(id GridWrapItemID) `json:"-"`
OnUnselected func(id GridWrapItemID) `json:"-"`
// Length is a callback for returning the number of items in the GridWrap.
Length func() int `json:"-"`

// CreateItem is a callback invoked to create a new widget to render
// an item in the GridWrap.
CreateItem func() fyne.CanvasObject `json:"-"`

// UpdateItem is a callback invoked to update a GridWrap item widget
// to display a new item in the list. The UpdateItem callback should
// only update the given item, it should not invoke APIs that would
// change other properties of the GridWrap itself.
UpdateItem func(id GridWrapItemID, item fyne.CanvasObject) `json:"-"`

// OnSelected is a callback to be notified when a given item
// in the GridWrap has been selected.
OnSelected func(id GridWrapItemID) `json:"-"`

// OnSelected is a callback to be notified when a given item
// in the GridWrap has been unselected.
OnUnselected func(id GridWrapItemID) `json:"-"`

currentFocus ListItemID
focused bool
Expand Down Expand Up @@ -507,36 +522,32 @@ type gridItemAndID struct {
}

type gridWrapLayout struct {
list *GridWrap
gw *GridWrap

itemPool async.Pool[fyne.CanvasObject]
slicePool async.Pool[*[]gridItemAndID]
visible []gridItemAndID
itemPool async.Pool[fyne.CanvasObject]
visible []gridItemAndID
wasVisible []gridItemAndID
}

func newGridWrapLayout(list *GridWrap) fyne.Layout {
l := &gridWrapLayout{list: list}
l.slicePool.New = func() *[]gridItemAndID {
s := make([]gridItemAndID, 0)
return &s
}
list.offsetUpdated = l.offsetUpdated
return l
func newGridWrapLayout(gw *GridWrap) fyne.Layout {
gwl := &gridWrapLayout{gw: gw}
gw.offsetUpdated = gwl.offsetUpdated
return gwl
}

func (l *gridWrapLayout) Layout(_ []fyne.CanvasObject, _ fyne.Size) {
l.updateGrid(true)
}

func (l *gridWrapLayout) MinSize(_ []fyne.CanvasObject) fyne.Size {
return l.list.contentMinSize()
return l.gw.contentMinSize()
}

func (l *gridWrapLayout) getItem() *gridWrapItem {
item := l.itemPool.Get()
if item == nil {
if f := l.list.CreateItem; f != nil {
child := createItemAndApplyThemeScope(f, l.list)
if f := l.gw.CreateItem; f != nil {
child := createItemAndApplyThemeScope(f, l.gw)

item = newGridWrapItem(child, nil)
}
Expand All @@ -545,17 +556,17 @@ func (l *gridWrapLayout) getItem() *gridWrapItem {
}

func (l *gridWrapLayout) offsetUpdated(pos fyne.Position) {
if l.list.offsetY == pos.Y {
if l.gw.offsetY == pos.Y {
return
}
l.list.offsetY = pos.Y
l.gw.offsetY = pos.Y
l.updateGrid(false)
}

func (l *gridWrapLayout) setupGridItem(li *gridWrapItem, id GridWrapItemID, focus bool) {
previousIndicator := li.selected
li.selected = false
for _, s := range l.list.selected {
for _, s := range l.gw.selected {
if id == s {
li.selected = true
break
Expand All @@ -568,21 +579,21 @@ func (l *gridWrapLayout) setupGridItem(li *gridWrapItem, id GridWrapItemID, focu
li.hovered = false
li.Refresh()
}
if f := l.list.UpdateItem; f != nil {
if f := l.gw.UpdateItem; f != nil {
f(id, li.child)
}
li.onTapped = func() {
if !fyne.CurrentDevice().IsMobile() {
l.list.RefreshItem(l.list.currentFocus)
canvas := fyne.CurrentApp().Driver().CanvasForObject(l.list)
l.gw.RefreshItem(l.gw.currentFocus)
canvas := fyne.CurrentApp().Driver().CanvasForObject(l.gw)
if canvas != nil {
canvas.Focus(l.list)
canvas.Focus(l.gw)
}

l.list.currentFocus = id
l.gw.currentFocus = id
}

l.list.Select(id)
l.gw.Select(id)
}
}

Expand All @@ -604,94 +615,78 @@ func (l *GridWrap) ColumnCount() int {

func (l *gridWrapLayout) updateGrid(refresh bool) {
// code here is a mashup of listLayout.updateList and gridWrapLayout.Layout
padding := l.list.Theme().Size(theme.SizeNamePadding)
padding := l.gw.Theme().Size(theme.SizeNamePadding)

length := 0
if f := l.list.Length; f != nil {
if f := l.gw.Length; f != nil {
length = f()
}

colCount := l.list.ColumnCount()
visibleRowsCount := int(math.Ceil(float64(l.list.scroller.Size().Height)/float64(l.list.itemMin.Height+padding))) + 1
colCount := l.gw.ColumnCount()
visibleRowsCount := int(math.Ceil(float64(l.gw.scroller.Size().Height)/float64(l.gw.itemMin.Height+padding))) + 1

offY := l.list.offsetY - float32(math.Mod(float64(l.list.offsetY), float64(l.list.itemMin.Height+padding)))
minRow := int(offY / (l.list.itemMin.Height + padding))
offY := l.gw.offsetY - float32(math.Mod(float64(l.gw.offsetY), float64(l.gw.itemMin.Height+padding)))
minRow := int(offY / (l.gw.itemMin.Height + padding))
minItem := GridWrapItemID(minRow * colCount)
maxRow := int(math.Min(float64(minRow+visibleRowsCount), math.Ceil(float64(length)/float64(colCount))))
maxItem := GridWrapItemID(math.Min(float64(maxRow*colCount), float64(length-1)))

if l.list.UpdateItem == nil {
if l.gw.UpdateItem == nil {
fyne.LogError("Missing UpdateCell callback required for GridWrap", nil)
}

// Keep pointer reference for copying slice header when returning to the pool
// https://blog.mike.norgate.xyz/unlocking-go-slice-performance-navigating-sync-pool-for-enhanced-efficiency-7cb63b0b453e
wasVisiblePtr := l.slicePool.Get()
wasVisible := (*wasVisiblePtr)[:0]
wasVisible = append(wasVisible, l.visible...)

oldVisibleLen := len(l.visible)
// l.wasVisible now represents the currently visible items, while
// l.visible will be updated to represent what is visible *after* the update
l.wasVisible = append(l.wasVisible, l.visible...)
l.visible = l.visible[:0]

c := l.list.scroller.Content.(*fyne.Container)
c := l.gw.scroller.Content.(*fyne.Container)
oldObjLen := len(c.Objects)
c.Objects = c.Objects[:0]
y := offY
curItemID := minItem
for row := minRow; row <= maxRow && curItemID <= maxItem; row++ {
x := float32(0)
for col := 0; col < colCount && curItemID <= maxItem; col++ {
item, ok := l.searchVisible(wasVisible, curItemID)
item, ok := l.searchVisible(l.wasVisible, curItemID)
if !ok {
item = l.getItem()
if item == nil {
continue
}
item.Resize(l.list.itemMin)
item.Resize(l.gw.itemMin)
}

item.Move(fyne.NewPos(x, y))
if refresh {
item.Resize(l.list.itemMin)
item.Resize(l.gw.itemMin)
}

x += l.list.itemMin.Width + padding
x += l.gw.itemMin.Width + padding
l.visible = append(l.visible, gridItemAndID{item: item, id: curItemID})
c.Objects = append(c.Objects, item)
curItemID++
}
y += l.list.itemMin.Height + padding
y += l.gw.itemMin.Height + padding
}
l.nilOldSliceData(c.Objects, len(c.Objects), oldObjLen)
l.nilOldVisibleSliceData(l.visible, len(l.visible), oldVisibleLen)

for _, old := range wasVisible {
for _, old := range l.wasVisible {
if _, ok := l.searchVisible(l.visible, old.id); !ok {
l.itemPool.Put(old.item)
}
}

// make a local deep copy of l.visible since rest of this function is unlocked
// and cannot safely access l.visible
visiblePtr := l.slicePool.Get()
visible := (*visiblePtr)[:0]
visible = append(visible, l.visible...)

for _, obj := range visible {
l.setupGridItem(obj.item, obj.id, l.list.focused && l.list.currentFocus == obj.id)
for _, obj := range l.visible {
l.setupGridItem(obj.item, obj.id, l.gw.focused && l.gw.currentFocus == obj.id)
}

// nil out all references before returning slices to pool
for i := 0; i < len(wasVisible); i++ {
wasVisible[i].item = nil
}
for i := 0; i < len(visible); i++ {
visible[i].item = nil
// we don't need wasVisible now until next call to update
// nil out all references before truncating slice
for i := 0; i < len(l.wasVisible); i++ {
l.wasVisible[i].item = nil
}
*wasVisiblePtr = wasVisible // Copy the slice header over to the heap
*visiblePtr = visible
l.slicePool.Put(wasVisiblePtr)
l.slicePool.Put(visiblePtr)
l.wasVisible = l.wasVisible[:0]
}

// invariant: visible is in ascending order of IDs
Expand All @@ -712,12 +707,3 @@ func (l *gridWrapLayout) nilOldSliceData(objs []fyne.CanvasObject, len, oldLen i
}
}
}

func (l *gridWrapLayout) nilOldVisibleSliceData(objs []gridItemAndID, len, oldLen int) {
if oldLen > len {
objs = objs[:oldLen] // gain view into old data
for i := len; i < oldLen; i++ {
objs[i].item = nil
}
}
}
Loading

0 comments on commit 817fbd8

Please sign in to comment.