ui: basic map scroller
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
package client
|
||||
|
||||
/*
|
||||
Fyne-friendly latest-wins coalescing for canvas.NewRaster(draw func(w,h int) image.Image).
|
||||
|
||||
How to use (integration sketch):
|
||||
|
||||
type editor struct {
|
||||
w *world.World
|
||||
drawer world.PrimitiveDrawer // wraps gg.Context over a backing *image.RGBA
|
||||
raster *canvas.Raster
|
||||
|
||||
co *client.RasterCoalescer[world.RenderParams]
|
||||
}
|
||||
|
||||
func (e *editor) initCoalescer() {
|
||||
exec := client.FyneMainThreadExecutor{} // uses fyne.CurrentApp().Driver().RunOnMain
|
||||
e.co = client.NewRasterCoalescer(exec, e.raster, func(wPx, hPx int, p world.RenderParams) image.Image {
|
||||
// your existing draw pipeline:
|
||||
// 1) ensure viewport/margins inside p are consistent with wPx/hPx if needed
|
||||
// 2) call e.w.Render(e.drawer, p)
|
||||
// 3) get image from gg.Context, crop margins, return
|
||||
_ = e.w.Render(e.drawer, p)
|
||||
return e.drawerImageCropped() // your code
|
||||
})
|
||||
}
|
||||
|
||||
// Call from input handlers (pan/zoom/etc). Can be from any goroutine.
|
||||
func (e *editor) RefreshUI(p world.RenderParams) {
|
||||
e.co.Request(p) // schedules raster.Refresh() on UI thread
|
||||
}
|
||||
|
||||
// Raster draw callback:
|
||||
func (e *editor) draw(wPx, hPx int) image.Image {
|
||||
return e.co.Draw(wPx, hPx)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user