package client import ( "image" "sync" "galaxy/client/updater" "galaxy/client/world" "galaxy/connector" mc "galaxy/model/client" "galaxy/model/report" "galaxy/storage" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) const version = "1.0.0" type client struct { s storage.UIStorage conn connector.UIConnector app fyne.App window fyne.Window loadReportFunc func(uint) world *world.World drawer *world.GGDrawer raster *canvas.Raster co *RasterCoalescer[world.RenderParams] pan *PanController // Protected camera/options state (UI-facing). This is the "base" params snapshot. // Viewport/margins are NOT stored here; they come from raster draw callback. mu sync.RWMutex wp *world.RenderParams canvasScale float32 // Latest raster geometry metadata for correct event->pixel conversion: // - logical size: raster.Size() (Fyne units) // - pixel size: last (wPx,hPx) passed to draw callback metaMu sync.RWMutex lastRasterLogicW float32 lastRasterLogicH float32 lastRasterPxW int lastRasterPxH int lastCanvasScale float32 // optional, useful for debugging // Snapshot of params actually used for the last render (includes viewport/margins). // Used for HitTest and to keep UI interactions consistent with what the user sees. lastRenderedMu sync.RWMutex lastRenderedParams world.RenderParams // Indexing / backing-canvas caches (owned by client because it depends on UI geometry) lastIndexedViewportW int lastIndexedViewportH int lastIndexedZoomFp int lastCanvasW int lastCanvasH int viewportImg *image.RGBA viewportW int viewportH int hits []world.Hit fullStorage storage.Storage fullConnector connector.Connector updater *updater.Manager backgroundStop chan struct{} backgroundOnce sync.Once onConnectionFn func(bool) onConnectionErrFn func(error) onStorageErrFn func(error) onServiceErrFn func(error) } func NewClient(s storage.UIStorage, conn connector.UIConnector, app fyne.App) (mc.Client, error) { e := &client{ s: s, conn: conn, app: app, window: app.NewWindow("Galaxy Plus"), world: nil, wp: &world.RenderParams{ CameraZoom: 1.0, Options: &world.RenderOptions{DisableWrapScroll: false}, }, lastCanvasScale: 1.0, hits: make([]world.Hit, 5), backgroundStop: make(chan struct{}), } if fullStorage, ok := s.(storage.Storage); ok { e.fullStorage = fullStorage } if fullConnector, ok := conn.(connector.Connector); ok { e.fullConnector = fullConnector } if e.fullStorage != nil && e.fullConnector != nil { e.updater = updater.NewManager(e.fullStorage, e.fullConnector) } e.loadReportFunc = e.loadReport e.drawer = &world.GGDrawer{DC: nil} e.raster = canvas.NewRaster(func(wPx, hPx int) image.Image { return e.draw(wPx, hPx) }) e.pan = NewPanController(e) e.co = NewRasterCoalescer( FyneExecutor{}, e.raster, func(wPx, hPx int, p world.RenderParams) image.Image { return e.renderRasterImage(wPx, hPx, p) }, ) return e, nil } func (e *client) loadReport(t uint) { e.conn.FetchReport("GAME_ID", t, func(r report.Report, err error) { if err != nil { e.handlerError(err) } else { e.setReport(r) } }) } func (e *client) setReport(r report.Report) { w := world.NewWorld(int(r.Width), int(r.Height)) for i := range r.LocalPlanet { p := r.LocalPlanet[i] w.AddCircle(p.X.F(), p.Y.F(), p.Size.F()) } for i := range r.UnidentifiedPlanet { p := r.UnidentifiedPlanet[i] w.AddPoint(p.X.F(), p.Y.F()) } e.loadWorld(w) } func (e *client) BuildUI(w fyne.Window) { mapCanvas := newInteractiveRaster(e, e.raster, e.onRasterWidgetLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped) mapCanvas.SetMinSize(fyne.NewSize(640, 480)) toolbar := widget.NewToolbar( widget.NewToolbarAction( theme.FolderIcon(), func() { e.loadWorld(mockWorld()) }), widget.NewToolbarSeparator(), widget.NewToolbarAction( theme.NavigateBackIcon(), func() {}), widget.NewToolbarAction( theme.NavigateNextIcon(), func() {}), ) tabs := container.NewAppTabs( container.NewTabItemWithIcon( "Map", theme.GridIcon(), mapCanvas), container.NewTabItemWithIcon( "Calculator", theme.ComputerIcon(), container.NewStack(widget.NewButton("Calc", func() {})), ), ) content := container.NewBorder( toolbar, // top nil, // bottom nil, // left nil, // right tabs, // center ) w.CenterOnScreen() w.SetContent(content) } func (e *client) loadWorld(w *world.World) { if w == nil { return } w.SetCircleRadiusScaleFp(world.SCALE / 4) e.world = w // TODO: store camera position in user settings e.wp.CameraXWorldFp = w.W / 2 e.wp.CameraYWorldFp = w.H / 2 e.world.SetTheme(world.ThemeDark) e.RequestRefresh() } func (e *client) Run() error { e.BuildUI(e.window) e.startBackground() e.RequestRefresh() e.window.SetMaster() e.window.Resize(fyne.NewSize(800, 600)) e.window.CenterOnScreen() e.window.ShowAndRun() e.stopBackground() return nil } func (e *client) Shutdown() { e.stopBackground() e.window.Close() } func (e *client) Version() string { return version } func (e *client) OnConnection(isGood bool) { if e.onConnectionFn != nil { e.onConnectionFn(isGood) } } func (e *client) OnConnectionError(err error) { if e.onConnectionErrFn != nil { e.onConnectionErrFn(err) } } func (e *client) OnStorageError(err error) { if e.onStorageErrFn != nil { e.onStorageErrFn(err) } } func (e *client) OnServiceError(err error) { if e.onServiceErrFn != nil { e.onServiceErrFn(err) } }