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 }