290 lines
6.8 KiB
Go
290 lines
6.8 KiB
Go
package client
|
|
|
|
import (
|
|
"image"
|
|
"sync"
|
|
|
|
"galaxy/client/updater"
|
|
"galaxy/client/widget/calculator"
|
|
"galaxy/client/world"
|
|
"galaxy/connector"
|
|
mc "galaxy/model/client"
|
|
"galaxy/storage"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/lang"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
)
|
|
|
|
const version = "1.0.0"
|
|
|
|
type client struct {
|
|
s storage.Storage
|
|
conn connector.Connector
|
|
app fyne.App
|
|
window fyne.Window
|
|
|
|
state *mc.State
|
|
stateMu sync.RWMutex
|
|
|
|
reg *registry
|
|
|
|
calculator *calculator.Calculator
|
|
mapSplitter *container.Split
|
|
accInfo *widget.AccordionItem
|
|
accCalc *widget.AccordionItem
|
|
|
|
// 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
|
|
|
|
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.Storage, conn connector.Connector, app fyne.App) (mc.Client, error) {
|
|
e := &client{
|
|
s: s,
|
|
conn: conn,
|
|
app: app,
|
|
window: app.NewWindow("Galaxy Plus"),
|
|
reg: newRegistry(),
|
|
lastCanvasScale: 1.0,
|
|
world: nil,
|
|
hits: make([]world.Hit, 5),
|
|
backgroundStop: make(chan struct{}),
|
|
}
|
|
e.calculator = calculator.NewCaclulator(calculator.WithCreateHandler(e.createShipClass))
|
|
e.updater = updater.NewManager(e.s, e.conn)
|
|
|
|
stateExists, err := e.s.StateExists()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if stateExists {
|
|
state, err := e.s.LoadState()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
e.state = &state
|
|
} else {
|
|
e.state = &mc.State{
|
|
ClientCurrentVersion: e.Version(),
|
|
CameraZoom: 1.0,
|
|
MapSplitterOffset: 0.5,
|
|
AccordionInfoOpen: false,
|
|
AccordionCalcOpen: false,
|
|
}
|
|
if err := e.s.SaveState(*e.state); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if e.state.CameraZoom <= 0 {
|
|
e.state.CameraZoom = 1.0
|
|
}
|
|
if e.state.MapSplitterOffset <= 0 {
|
|
e.state.MapSplitterOffset = 0.5
|
|
}
|
|
e.wp = &world.RenderParams{
|
|
Options: &world.RenderOptions{DisableWrapScroll: false},
|
|
CameraZoom: e.state.CameraZoom,
|
|
CameraXWorldFp: e.state.CameraXFp,
|
|
CameraYWorldFp: e.state.CameraYFp,
|
|
}
|
|
|
|
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) BuildUI(w fyne.Window) {
|
|
mapCanvasObject := newInteractiveRaster(e.raster, e.onRasterWidgetLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped)
|
|
|
|
toolbar := widget.NewToolbar(
|
|
widget.NewToolbarAction(
|
|
theme.FolderIcon(),
|
|
func() { e.initReportAsync("GAME_ID", 0) }),
|
|
widget.NewToolbarSeparator(),
|
|
widget.NewToolbarAction(
|
|
theme.NavigateBackIcon(),
|
|
func() {}),
|
|
widget.NewToolbarAction(
|
|
theme.NavigateNextIcon(),
|
|
func() {}),
|
|
)
|
|
|
|
e.accInfo = widget.NewAccordionItem(lang.L("title.info"), container.NewStack())
|
|
e.accInfo.Open = e.state.AccordionInfoOpen
|
|
e.accCalc = widget.NewAccordionItem(lang.L("title.calculator"), e.calculator.CanvasObject)
|
|
e.accCalc.Open = e.state.AccordionCalcOpen
|
|
|
|
accordion := widget.NewAccordion()
|
|
accordion.MultiOpen = true
|
|
accordion.Append(e.accCalc)
|
|
accordion.Append(e.accInfo)
|
|
|
|
e.mapSplitter = container.NewHSplit(mapCanvasObject, container.NewHScroll(accordion))
|
|
e.mapSplitter.SetOffset(e.state.MapSplitterOffset)
|
|
|
|
tabs := container.NewAppTabs(
|
|
container.NewTabItemWithIcon(
|
|
lang.L("title.map"),
|
|
theme.GridIcon(),
|
|
e.mapSplitter),
|
|
container.NewTabItemWithIcon(
|
|
"Calculator",
|
|
theme.ComputerIcon(),
|
|
container.NewStack(widget.NewButton("Calc", func() {})),
|
|
),
|
|
)
|
|
|
|
th := tabs.Theme()
|
|
icon := canvas.NewImageFromResource(th.Icon(theme.IconNameInfo))
|
|
|
|
statusLeft := widget.NewTextGridFromString("Status")
|
|
statusAd := widget.NewTextGridFromString("")
|
|
|
|
statusBar := container.NewBorder(
|
|
nil, // top
|
|
nil, // bottom
|
|
container.NewHBox(statusLeft, widget.NewSeparator()), // left
|
|
container.NewHBox(widget.NewSeparator(), icon), // right
|
|
statusAd, // center
|
|
)
|
|
|
|
content := container.NewBorder(
|
|
toolbar, // top
|
|
statusBar, // bottom
|
|
nil, // left
|
|
nil, // right
|
|
tabs, // center
|
|
)
|
|
|
|
w.CenterOnScreen()
|
|
w.SetContent(content)
|
|
s := statusBar.Size()
|
|
icon.SetMinSize(fyne.NewSize(s.Height, s.Height))
|
|
e.initLatestReport()
|
|
}
|
|
|
|
func (e *client) loadWorld(w *world.World) {
|
|
if w == nil {
|
|
return
|
|
}
|
|
w.SetCircleRadiusScaleFp(world.SCALE / 1000)
|
|
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.SetOnClosed(e.Shutdown)
|
|
e.window.ShowAndRun()
|
|
return nil
|
|
}
|
|
|
|
func (e *client) Shutdown() {
|
|
e.stopBackground()
|
|
e.ensureStatePersist()
|
|
e.window.Close()
|
|
}
|
|
|
|
// TODO: remove func?
|
|
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)
|
|
}
|
|
}
|