package client import ( "image" "sync" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/layout" "github.com/iliadenisov/galaxy/client/world" ) type Editor interface { BuildUI(fyne.Window) } type editor struct { world *world.World drawer *world.GGDrawer raster *canvas.Raster canvasScale float32 canvas *interactiveRaster win fyne.Window // Coalescer for latest-wins refresh scheduling. co *RasterCoalescer[world.RenderParams] pan *PanController // Protected render params state. Stored as value to avoid aliasing issues. mu sync.RWMutex wp *world.RenderParams // Last viewport size we indexed the world for. lastViewportW int lastViewportH int // Optional: you can keep the last expanded canvas size to avoid reallocations. lastCanvasW int lastCanvasH int // Reusable viewport buffer to avoid per-frame allocations. viewportImg *image.RGBA viewportW int viewportH int } func (e *editor) CanvasScale() float32 { return e.canvasScale } func (e *editor) ForceFullRedraw() { e.world.ForceFullRedrawNext() } func (e *editor) buildUI() fyne.CanvasObject { return e.canvas } // здесь определяю, изменились ли границы raster, если да - обновляю размеры viewport, margin и корректирую zoom func (e *editor) updateSizes() { canvas := fyne.CurrentApp().Driver().CanvasForObject(e.raster) if canvas == nil { return } size := e.raster.Size() e.canvasScale = canvas.Scale() width := int(size.Width * e.canvasScale) height := int(size.Height * e.canvasScale) if width > 0 && height > 0 && (width != e.wp.ViewportWidthPx || height != e.wp.ViewportHeightPx) { e.wp.ViewportWidthPx = width e.wp.ViewportHeightPx = height e.wp.MarginXPx = e.wp.ViewportWidthPx / 4 e.wp.MarginYPx = e.wp.ViewportHeightPx / 4 e.wp.CameraZoom = e.world.CorrectCameraZoom(e.wp.CameraZoom, e.wp.ViewportWidthPx, e.wp.ViewportHeightPx) e.world.IndexOnViewportChange(e.wp.ViewportWidthPx, e.wp.ViewportHeightPx, e.wp.CameraZoom) e.co.Request(*e.wp) } } func (e *editor) onDragged(ev *fyne.DragEvent) { e.pan.Dragged(ev) } func (e *editor) onDradEnd() { e.pan.DragEnd() } func (e *editor) wheelZoom(stepDelta int) {} func (e *editor) InitImage() { s := fyne.NewSize(292, 292) e.canvas.SetMinSize(s) e.updateSizes() } func (e *editor) onMapLayout(s fyne.Size) { e.updateSizes() } func (e *editor) BuildUI(w fyne.Window) { e.win = w content := container.New(layout.NewStackLayout(), e.buildUI()) w.CenterOnScreen() w.SetContent(content) } func NewEditor() *editor { w := world.NewWorld(300, 300) testWorldInit(w) e := &editor{ world: w, wp: &world.RenderParams{ CameraZoom: 1.0, CameraXWorldFp: 300 * world.SCALE, CameraYWorldFp: 300 * world.SCALE, // Viewport sizes and margins will be filled from draw(w,h). }, canvasScale: 1.0, } // Create a drawer with some initial context; real size will be adjusted on first draw. e.drawer = &world.GGDrawer{DC: nil} // Create raster; its draw callback delegates to coalescer. e.raster = canvas.NewRaster(func(wPx, hPx int) image.Image { return e.draw(wPx, hPx) }) e.canvas = newInteractiveRaster(e, e.raster, e.onMapLayout, e.onDragged, e.onDradEnd) e.pan = NewPanController(e) // Wire coalescer: it schedules raster.Refresh() on UI thread and renders once per draw call. exec := FyneExecutor{} e.co = NewRasterCoalescer( exec, e.raster, // Refresher func(wPx, hPx int, p world.RenderParams) image.Image { // This runs on UI thread (inside draw). It must return an image. return e.renderRasterImage(wPx, hPx, p) }, ) // Kick initial draw. e.RequestRefresh() e.InitImage() return e } func testWorldInit(w *world.World) { if _, err := w.AddCircle(150, 150, 50); err != nil { panic(err) } if _, err := w.AddCircle(150, 299, 30); err != nil { panic(err) } if _, err := w.AddCircle(299, 150, 30); err != nil { panic(err) } if _, err := w.AddLine(100, 20, 200, 30); err != nil { panic(err) } if _, err := w.AddLine(50, 50, 250, 100); err != nil { panic(err) } if _, err := w.AddPoint(10, 10); err != nil { panic(err) } if _, err := w.AddPoint(25, 255); err != nil { panic(err) } }