134 lines
3.5 KiB
Go
134 lines
3.5 KiB
Go
package client
|
|
|
|
/*
|
|
Fyne-friendly latest-wins coalescing for canvas.NewRaster(draw func(w,h int) image.Image).
|
|
|
|
Key property:
|
|
- draw() renders at most once per invocation (never loops).
|
|
- if new requests arrived while drawing, we schedule exactly one extra Refresh.
|
|
*/
|
|
|
|
import (
|
|
"image"
|
|
"sync"
|
|
)
|
|
|
|
// UIExecutor posts a function to run on the UI/main thread.
|
|
type UIExecutor interface {
|
|
Post(fn func())
|
|
}
|
|
|
|
// Refresher is the minimal interface we need from fyne.CanvasObject / Raster.
|
|
type Refresher interface {
|
|
Refresh()
|
|
}
|
|
|
|
// RasterRenderer renders the latest params and returns an image.
|
|
// Must be called on the UI thread (inside draw callback).
|
|
type RasterRenderer[P any] func(wPx, hPx int, params P) image.Image
|
|
|
|
// RasterCoalescer implements latest-wins coalescing for raster rendering.
|
|
// It is designed specifically for toolkits like fyne where the system calls draw(w,h)
|
|
// and expects a returned image.
|
|
type RasterCoalescer[P any] struct {
|
|
exec UIExecutor
|
|
refresher Refresher
|
|
renderer RasterRenderer[P]
|
|
|
|
mu sync.Mutex
|
|
|
|
// inDraw == true while Draw() is running on UI thread.
|
|
inDraw bool
|
|
|
|
// refreshQueued == true when we have already posted a Refresh() that has not yet
|
|
// resulted in a Draw() call (or is expected to call Draw soon).
|
|
refreshQueued bool
|
|
|
|
// pending == true when new params arrived while inDraw==true.
|
|
// Draw() will schedule exactly one follow-up Refresh after it returns.
|
|
pending bool
|
|
|
|
latest P
|
|
have bool
|
|
}
|
|
|
|
// NewRasterCoalescer creates a new coalescer.
|
|
// - exec.Post must run fn on UI thread.
|
|
// - refresher.Refresh will trigger the framework to call draw(w,h).
|
|
func NewRasterCoalescer[P any](exec UIExecutor, refresher Refresher, renderer RasterRenderer[P]) *RasterCoalescer[P] {
|
|
if exec == nil {
|
|
panic("RasterCoalescer: nil executor")
|
|
}
|
|
if refresher == nil {
|
|
panic("RasterCoalescer: nil refresher")
|
|
}
|
|
if renderer == nil {
|
|
panic("RasterCoalescer: nil renderer")
|
|
}
|
|
return &RasterCoalescer[P]{exec: exec, refresher: refresher, renderer: renderer}
|
|
}
|
|
|
|
// Request stores the latest params and schedules exactly one refresh (latest-wins).
|
|
// Can be called from any goroutine.
|
|
func (c *RasterCoalescer[P]) Request(params P) {
|
|
c.mu.Lock()
|
|
c.latest = params
|
|
c.have = true
|
|
|
|
// If we are currently inside Draw(), don't schedule refresh immediately.
|
|
// Just mark pending; Draw() will schedule one follow-up refresh after it returns.
|
|
if c.inDraw {
|
|
c.pending = true
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// Not drawing. Schedule at most one refresh until the next Draw() happens.
|
|
if c.refreshQueued {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
c.refreshQueued = true
|
|
c.mu.Unlock()
|
|
|
|
c.exec.Post(c.refresher.Refresh)
|
|
}
|
|
|
|
// Draw must be called from the raster draw callback on the UI thread.
|
|
// It renders exactly once with the latest snapshot.
|
|
// If more requests arrived while drawing, it schedules exactly one extra refresh.
|
|
func (c *RasterCoalescer[P]) Draw(wPx, hPx int) image.Image {
|
|
c.mu.Lock()
|
|
// A Draw call corresponds to a previously scheduled refresh being serviced.
|
|
c.refreshQueued = false
|
|
|
|
if !c.have {
|
|
c.mu.Unlock()
|
|
return image.NewRGBA(image.Rect(0, 0, wPx, hPx))
|
|
}
|
|
|
|
c.inDraw = true
|
|
c.pending = false
|
|
params := c.latest
|
|
c.mu.Unlock()
|
|
|
|
img := c.renderer(wPx, hPx, params)
|
|
|
|
c.mu.Lock()
|
|
needAnother := c.pending
|
|
c.pending = false
|
|
c.inDraw = false
|
|
|
|
// If we need another frame, schedule exactly one refresh (if not already queued).
|
|
if needAnother && !c.refreshQueued {
|
|
c.refreshQueued = true
|
|
c.mu.Unlock()
|
|
c.exec.Post(c.refresher.Refresh)
|
|
return img
|
|
}
|
|
|
|
c.mu.Unlock()
|
|
return img
|
|
}
|