ui calculator

This commit is contained in:
Ilia Denisov
2026-03-30 19:38:24 +02:00
committed by GitHub
parent 17f366cd6b
commit a7793f5416
37 changed files with 2046 additions and 270 deletions
+7 -19
View File
@@ -1,22 +1,10 @@
# Client for Galaxy Plus
## Ship Calculator
UI Client is capable of:
```text
Class: [ ] { Create }
Drives: [20.000] x 1.013 [O--]
Weapons: [ 0.000] x [1.000] [==0]
Armament: [ 0 ]
Schields: [ 5.500] @ [1.123]
Cargo: [30.125] @ [1.320]
Mass: [ 123,45 ] [==0]
Speed: ( 12,456 ) [O--]
Attack: 0
Defense: 100,0
Planet { Name } Production:
[ 100.0 ] MAT per turn produced [O--] supplied
{ N.000 } ship(s) per turn
{ M.000 } turn(s) per ship
```
- Register a new player and login for an existing player using only e-mail and one-time codes,
- Enlist to a new Game from available onboard Games list,
- Request list of Games in which Player participating,
- Request, store and display particular Game data,
- Use push-like mechanism for receiving asynchronous updates from Server,
- Offline mode when no internet connection is available or user desired to work offline.
+50 -3
View File
@@ -1,6 +1,7 @@
package client
import (
"fmt"
"time"
gerr "galaxy/error"
@@ -9,10 +10,11 @@ import (
var (
checkConnectionInterval = 5 * time.Second
checkVersionInterval = time.Hour
statePersistInterval = time.Second
)
func (e *client) startBackground() {
if e.fullConnector == nil && e.updater == nil {
if e.conn == nil || e.updater == nil {
return
}
@@ -28,9 +30,11 @@ func (e *client) stopBackground() {
func (e *client) backgroundLoop() {
checkConnTimer := time.NewTimer(checkConnectionInterval)
checkVersionTimer := time.NewTimer(checkVersionInterval)
persistStateTimer := time.NewTimer(statePersistInterval)
defer func() {
checkConnTimer.Stop()
checkVersionTimer.Stop()
persistStateTimer.Stop()
}()
for {
@@ -38,8 +42,8 @@ func (e *client) backgroundLoop() {
case <-e.backgroundStop:
return
case <-checkConnTimer.C:
if e.fullConnector != nil {
e.OnConnection(e.fullConnector.CheckConnection())
if e.conn != nil {
e.OnConnection(e.conn.CheckConnection())
}
checkConnTimer.Reset(checkConnectionInterval)
case <-checkVersionTimer.C:
@@ -49,15 +53,58 @@ func (e *client) backgroundLoop() {
}
}
checkVersionTimer.Reset(checkVersionInterval)
case <-persistStateTimer.C:
e.ensureStatePersist()
persistStateTimer.Reset(statePersistInterval)
}
}
}
func (e *client) ensureStatePersist() {
param := e.GetParams()
needSaving := false
e.stateMu.Lock()
if e.world != nil {
if param.CameraZoom > 0 && param.CameraZoom != e.state.CameraZoom {
e.state.CameraZoom = param.CameraZoom
needSaving = true
}
if param.CameraXWorldFp != e.state.CameraXFp {
e.state.CameraXFp = param.CameraXWorldFp
needSaving = true
}
if param.CameraYWorldFp != e.state.CameraYFp {
e.state.CameraYFp = param.CameraYWorldFp
needSaving = true
}
}
if e.mapSplitter != nil && e.mapSplitter.Offset != e.state.MapSplitterOffset {
e.state.MapSplitterOffset = e.mapSplitter.Offset
needSaving = true
}
if e.accInfo.Open != e.state.AccordionInfoOpen {
e.state.AccordionInfoOpen = e.accInfo.Open
needSaving = true
}
if e.accCalc.Open != e.state.AccordionCalcOpen {
e.state.AccordionCalcOpen = e.accCalc.Open
needSaving = true
}
if needSaving {
if err := e.s.SaveState(*e.state); err != nil {
e.handlerError(err)
}
}
e.stateMu.Unlock()
}
func (e *client) handlerError(err error) {
if err == nil {
return
}
fmt.Printf("ERROR: %s\n", err)
switch {
case gerr.IsConnection(err):
e.OnConnectionError(err)
-3
View File
@@ -11,7 +11,6 @@ import (
type interactiveRaster struct {
widget.BaseWidget
edit *client
min fyne.Size
raster *canvas.Raster
onLayout func(fyne.Size)
@@ -50,7 +49,6 @@ func (r *interactiveRaster) Tapped(ev *fyne.PointEvent) {
func (r *interactiveRaster) TappedSecondary(*fyne.PointEvent) {}
func newInteractiveRaster(
edit *client,
raster *canvas.Raster,
onLayout func(fyne.Size),
onScrolled func(*fyne.ScrollEvent),
@@ -60,7 +58,6 @@ func newInteractiveRaster(
) *interactiveRaster {
r := &interactiveRaster{
raster: raster,
edit: edit,
onLayout: onLayout,
onScrolled: onScrolled,
onDragged: onDragged,
+100 -63
View File
@@ -5,15 +5,16 @@ import (
"sync"
"galaxy/client/updater"
"galaxy/client/widget/calculator"
"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/lang"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
@@ -21,12 +22,22 @@ import (
const version = "1.0.0"
type client struct {
s storage.UIStorage
conn connector.UIConnector
s storage.Storage
conn connector.Connector
app fyne.App
window fyne.Window
loadReportFunc func(uint)
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
@@ -69,8 +80,6 @@ type client struct {
hits []world.Hit
fullStorage storage.Storage
fullConnector connector.Connector
updater *updater.Manager
backgroundStop chan struct{}
backgroundOnce sync.Once
@@ -81,32 +90,55 @@ type client struct {
onServiceErrFn func(error)
}
func NewClient(s storage.UIStorage, conn connector.UIConnector, app fyne.App) (mc.Client, 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"),
world: nil,
wp: &world.RenderParams{
CameraZoom: 1.0,
Options: &world.RenderOptions{DisableWrapScroll: false},
},
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{}),
}
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.calculator = calculator.NewCaclulator(calculator.WithCreateHandler(e.createShipClass))
e.updater = updater.NewManager(e.s, e.conn)
e.loadReportFunc = e.loadReport
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}
@@ -127,40 +159,13 @@ func NewClient(s storage.UIStorage, conn connector.UIConnector, app fyne.App) (m
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))
mapCanvasObject := newInteractiveRaster(e.raster, e.onRasterWidgetLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped)
toolbar := widget.NewToolbar(
widget.NewToolbarAction(
theme.FolderIcon(),
func() {
e.loadReport(0)
// e.loadWorld(mockWorld())
}),
func() { e.initReportAsync("GAME_ID", 0) }),
widget.NewToolbarSeparator(),
widget.NewToolbarAction(
theme.NavigateBackIcon(),
@@ -170,11 +175,24 @@ func (e *client) BuildUI(w fyne.Window) {
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(
"Map",
lang.L("title.map"),
theme.GridIcon(),
mapCanvas),
e.mapSplitter),
container.NewTabItemWithIcon(
"Calculator",
theme.ComputerIcon(),
@@ -182,16 +200,33 @@ func (e *client) BuildUI(w fyne.Window) {
),
)
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
nil, // bottom
nil, // left
nil, // right
tabs, // center
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) {
@@ -215,16 +250,18 @@ func (e *client) Run() error {
e.window.SetMaster()
e.window.Resize(fyne.NewSize(800, 600))
e.window.CenterOnScreen()
e.window.SetOnClosed(e.Shutdown)
e.window.ShowAndRun()
e.stopBackground()
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) {
+6 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"galaxy/client"
"galaxy/client/appmeta"
"galaxy/client/loader"
"galaxy/connector/http"
@@ -12,6 +13,7 @@ import (
"os/signal"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/lang"
)
func main() {
@@ -19,7 +21,7 @@ func main() {
defer func() {
if err == nil {
if r := recover(); r != nil {
err = errors.Join(err, fmt.Errorf("app panics: %v", r))
err = errors.Join(err, fmt.Errorf("panic: %v", r))
}
}
if err != nil {
@@ -32,6 +34,9 @@ func main() {
defer cancel()
app := app.NewWithID(appmeta.AppID)
if err = lang.AddTranslationsFS(client.Translations, "resource/lang"); err != nil {
return
}
s, err := fs.NewFS(app.Storage().RootURI().Path())
if err != nil {
return
@@ -44,6 +49,5 @@ func main() {
if err != nil {
return
}
err = l.Run(ctx)
}
+6 -2
View File
@@ -4,14 +4,15 @@ import (
"context"
"errors"
"fmt"
"galaxy/client/appmeta"
"galaxy/client"
"galaxy/client/appmeta"
"galaxy/connector/http"
"galaxy/storage/fs"
"os"
"os/signal"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/lang"
)
func main() {
@@ -19,7 +20,7 @@ func main() {
defer func() {
if err == nil {
if r := recover(); r != nil {
err = errors.Join(err, fmt.Errorf("app panics: %v", r))
err = errors.Join(err, fmt.Errorf("panic: %v", r))
}
}
if err != nil {
@@ -31,6 +32,9 @@ func main() {
defer cancel()
app := app.NewWithID(appmeta.AppID)
if err = lang.AddTranslationsFS(client.Translations, "resource/lang"); err != nil {
return
}
s, err := fs.NewFS(app.Storage().RootURI().Path())
if err != nil {
return
+6
View File
@@ -0,0 +1,6 @@
package client
import "embed"
//go:embed resource/lang
var Translations embed.FS
+61
View File
@@ -0,0 +1,61 @@
package client
import (
"galaxy/client/world"
"fyne.io/fyne/v2"
)
var m = func(v int) float64 { return float64(v) / float64(world.SCALE) }
func (e *client) onTapped(ev *fyne.PointEvent) {
if e.world == nil || ev == nil {
return
}
xPx, yPx, ok := e.eventPosToPixel(ev.Position.X, ev.Position.Y)
if !ok {
return
}
params := e.getLastRenderedParams()
hits, err := e.world.HitTest(e.hits, &params, xPx, yPx)
if err != nil {
e.handlerError(err)
return
}
if len(hits) == 0 {
e.calculator.UnloadPlanet()
return
}
for i := range hits {
e.onHit(hits[i])
}
}
func (e *client) onHit(hit world.Hit) {
// var coord string
// if hit.Kind == world.KindLine {
// coord = fmt.Sprintf("{%f,%f - %f,%f}", m(hit.X1), m(hit.Y1), m(hit.X2), m(hit.Y2))
// } else {
// coord = fmt.Sprintf("{%f,%f}", m(hit.X), m(hit.Y))
// }
// fmt.Println("hit:", hit.ID, "Coord:", coord)
switch hit.Kind {
case world.KindPoint:
case world.KindCircle:
e.onHitCircle(hit.ID)
case world.KindLine:
}
}
func (e *client) onHitCircle(id world.PrimitiveID) {
p, ok := e.reg.localPlanet(id)
if !ok {
return
}
e.calculator.LoadPlanet(p.Name, p.Number, p.FreeIndustry.F(), p.Material.F(), p.Resources.F())
e.calculator.Refresh()
}
+76
View File
@@ -0,0 +1,76 @@
package client
import (
"galaxy/client/world"
"galaxy/model/report"
)
const (
entityClassUnknown int = iota - 1
entityClassLocalPlanet
entityClassOthersPlanet
entityClassFreePlanet
entityClassUnidentifiedPlanet
)
type registry struct {
report *report.Report
localPlanetIndex map[world.PrimitiveID]int
unidentifiedPlanetIndex map[world.PrimitiveID]int
}
func newRegistry() *registry {
return &registry{
localPlanetIndex: make(map[world.PrimitiveID]int),
unidentifiedPlanetIndex: make(map[world.PrimitiveID]int),
}
}
func (r *registry) clear(report *report.Report) {
r.report = report
clear(r.localPlanetIndex)
clear(r.unidentifiedPlanetIndex)
}
func (r *registry) entityClass(id world.PrimitiveID) int {
if r.isLocalPlanet(id) {
return entityClassLocalPlanet
}
if r.isUnidentifiedPlanet(id) {
return entityClassUnidentifiedPlanet
}
return entityClassUnknown
}
func (r *registry) registerLocalPlanet(id world.PrimitiveID, index int) {
r.localPlanetIndex[id] = index
}
func (r *registry) isLocalPlanet(id world.PrimitiveID) bool {
_, ok := r.localPlanetIndex[id]
return ok
}
func (r *registry) localPlanet(id world.PrimitiveID) (*report.LocalPlanet, bool) {
i, ok := r.localPlanetIndex[id]
if !ok {
return nil, false
}
if i > len(r.report.LocalPlanet)-1 {
return nil, false
}
return &r.report.LocalPlanet[i], true
}
func (r *registry) registerUnidentifiedPlanet(id world.PrimitiveID, index int) {
r.unidentifiedPlanetIndex[id] = index
}
func (r *registry) isUnidentifiedPlanet(id world.PrimitiveID) bool {
_, ok := r.unidentifiedPlanetIndex[id]
return ok
}
func (c *client) createShipClass(n string, D float64, A uint, W float64, S float64, C float64) {
}
+119
View File
@@ -0,0 +1,119 @@
package client
import (
"fmt"
"galaxy/client/widget/calculator"
"galaxy/client/world"
mc "galaxy/model/client"
"galaxy/model/report"
"slices"
"fyne.io/fyne/v2"
)
func (e *client) initLatestReport() {
e.stateMu.Lock()
if e.state.ActiveGameID != nil {
if stateIdx := slices.IndexFunc(e.state.GameState, func(gs mc.GameState) bool { return gs.ID == *e.state.ActiveGameID }); stateIdx >= 0 {
e.initReportAsync(*e.state.ActiveGameID, e.state.GameState[stateIdx].ActiveTurn)
}
}
e.stateMu.Unlock()
}
func (e *client) initReportAsync(gid mc.GameID, t uint) {
e.s.ReportExistsAsync(gid, t, func(b bool, err error) { e.reportAtStorageExists(gid, t, b, err) })
}
func (e *client) reportAtStorageExists(gid mc.GameID, t uint, exists bool, err error) {
if err != nil {
e.handlerError(err)
return
}
if exists {
e.s.LoadReportAsync(gid, t, func(r report.Report, err error) { e.loadReportHandler(gid, r, err) })
return
}
e.conn.FetchReport(gid, t, func(r report.Report, err error) { e.fetchReportHandler(gid, r, err) })
}
func (e *client) fetchReportHandler(gid mc.GameID, r report.Report, err error) {
if err != nil {
e.handlerError(err)
return
}
e.s.SaveReportAsync(gid, r.Turn, r, func(err error) { e.loadReportHandler(gid, r, err) })
}
func (e *client) loadReportHandler(gid mc.GameID, r report.Report, err error) {
if err != nil {
e.handlerError(err)
return
}
e.stateMu.Lock()
needSaveState := false
stateIdx := slices.IndexFunc(e.state.GameState, func(gs mc.GameState) bool { return gs.ID == gid })
if stateIdx < 0 {
e.state.GameState = append(e.state.GameState, mc.GameState{ID: gid, LastTurn: r.Turn, ActiveTurn: r.Turn})
stateIdx = len(e.state.GameState) - 1
needSaveState = true
}
if e.state.ActiveGameID == nil {
e.state.ActiveGameID = new(gid)
needSaveState = true
}
if e.state.GameState[stateIdx].LastTurn < r.Turn {
e.state.GameState[stateIdx].LastTurn = r.Turn
e.state.GameState[stateIdx].ActiveTurn = r.Turn
needSaveState = true
}
if needSaveState {
if err := e.s.SaveState(*e.state); err != nil {
e.handlerError(err)
return
}
}
e.stateMu.Unlock()
e.setReport(r)
}
func (e *client) setReport(r report.Report) {
w := world.NewWorld(int(r.Width), int(r.Height))
e.reg.clear(&r)
for i := range r.LocalPlanet {
p := r.LocalPlanet[i]
id, err := w.AddCircle(p.X.F(), p.Y.F(), p.Size.F(), world.CircleWithClass(world.CircleClassLocalPlanet))
if err != nil {
e.handlerError(err)
return
}
e.reg.registerLocalPlanet(id, i)
}
for i := range r.UnidentifiedPlanet {
p := r.UnidentifiedPlanet[i]
id, err := w.AddPoint(p.X.F(), p.Y.F(), world.PointWithClass(world.PointClassTrackIncoming))
if err != nil {
e.handlerError(err)
return
}
e.reg.registerUnidentifiedPlanet(id, i)
}
e.loadWorld(w)
selfIdx := slices.IndexFunc(r.Player, func(p report.Player) bool { return p.Name == r.Race })
if selfIdx >= 0 {
fyne.Do(func() {
e.calculator.Init(
calculator.WithPlayerDrives(r.Player[selfIdx].Drive.F()),
calculator.WithPlayerWeapons(r.Player[selfIdx].Weapons.F()),
calculator.WithPlayerShields(r.Player[selfIdx].Shields.F()),
calculator.WithPlayerCargo(r.Player[selfIdx].Cargo.F()),
)
})
} else {
e.OnServiceError(fmt.Errorf("race %q not found at report players list", r.Race))
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"title": {
"map": "Map",
"calculator": "Ship Calculator",
"info": "Info"
},
"planet": {
"title": "Planet #{{.Number}} '{{.Name}}' production fot this ship:",
"mat": "Materials",
"prod.mass": "Prod. Mass",
"prod.ships": "Ships"
},
"tech": {
"d": "Drive",
"w": "Weapons",
"s": "Shields",
"c": "Cargo"
},
"ship": {
"action.create": "Create",
"mass": "Mass",
"speed": "Speed",
"attack": "Attack",
"defense": "Defense",
"load": "Load"
},
"label": {
"max": "Max."
}
}
-31
View File
@@ -1,7 +1,6 @@
package client
import (
"fmt"
"image"
"math"
@@ -256,36 +255,6 @@ func (e *client) onDradEnd() {
e.pan.DragEnd()
}
func (e *client) onTapped(ev *fyne.PointEvent) {
if e.world == nil || ev == nil {
return
}
xPx, yPx, ok := e.eventPosToPixel(ev.Position.X, ev.Position.Y)
if !ok {
return
}
params := e.getLastRenderedParams()
hits, err := e.world.HitTest(e.hits, &params, xPx, yPx)
if err != nil {
// In UI you probably don't want panic; keep your existing handling.
panic(err)
}
m := func(v int) float64 { return float64(v) / float64(world.SCALE) }
for _, hit := range hits {
var coord string
if hit.Kind == world.KindLine {
coord = fmt.Sprintf("{%f,%f - %f,%f}", m(hit.X1), m(hit.Y1), m(hit.X2), m(hit.Y2))
} else {
coord = fmt.Sprintf("{%f,%f}", m(hit.X), m(hit.Y))
}
fmt.Println("hit:", hit.ID, "Coord:", coord)
}
}
func (e *client) onScrolled(s *fyne.ScrollEvent) {
if e.world == nil || s == nil {
return
+629
View File
@@ -0,0 +1,629 @@
package calculator
import (
"errors"
"galaxy/calc"
"galaxy/client/widget/numeric"
"galaxy/util"
"slices"
"strconv"
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/widget"
)
type CalculatorOpt func(*Calculator)
type ShipClass struct {
Name string
Drive float64
Armament uint
Weapons float64
Shields float64
Cargo float64
}
type ShipClassFn func(string, float64, uint, float64, float64, float64)
type Calculator struct {
CanvasObject fyne.CanvasObject
playerDrivesTech float64
playerWeaponsTech float64
playerShieldsTech float64
playerCargoTech float64
shipDriveEntry *numeric.FloatEntry
shipWeaponsEntry *numeric.FloatEntry
shipArmamentEntry *numeric.IntEntry
shipShieldsEntry *numeric.FloatEntry
shipCargoEntry *numeric.FloatEntry
playerDrivesTechEntry *numeric.FloatEntry
playerWeaponsTechEntry *numeric.FloatEntry
playerShieldsTechEntry *numeric.FloatEntry
playerCargoTechEntry *numeric.FloatEntry
drivesTechOverride *widget.Check
weaponsTechOverride *widget.Check
shieldsTechOverride *widget.Check
cargoTechOverride *widget.Check
massEntry *numeric.FloatEntry
speedEntry *numeric.FloatEntry
attackEntry *numeric.FloatEntry
defenseEntry *numeric.FloatEntry
cargoLoadEntry *numeric.FloatEntry
planetMatEntry *numeric.FloatEntry
massOverride *widget.Check
speedOverride *widget.Check
attackOverride *widget.Check
defenseOverride *widget.Check
cargoLoadMaximize *widget.Check
planetMatOverride *widget.Check
planetLabel *widget.Label
planetMassProdLabel *widget.Label
planetShipsProdLabel *widget.Label
planetContainer fyne.CanvasObject
planetProdContainer fyne.CanvasObject
shipSelector *widget.SelectEntry
shipCreateButton *widget.Button
onCreateHandler ShipClassFn
loader ShipClassFn
knownClasses []ShipClass
validateMu sync.RWMutex
l, mat, res float64
Valid bool
}
func WithPlayerDrives(v float64) CalculatorOpt {
return func(c *Calculator) { c.playerDrivesTech = v }
}
func WithPlayerWeapons(v float64) CalculatorOpt {
return func(c *Calculator) { c.playerWeaponsTech = v }
}
func WithPlayerShields(v float64) CalculatorOpt {
return func(c *Calculator) { c.playerShieldsTech = v }
}
func WithPlayerCargo(v float64) CalculatorOpt {
return func(c *Calculator) { c.playerCargoTech = v }
}
func WithCreateHandler(f ShipClassFn) CalculatorOpt {
return func(c *Calculator) { c.onCreateHandler = f }
}
func NewCaclulator(opts ...CalculatorOpt) *Calculator {
c := &Calculator{}
c.shipCreateButton = widget.NewButton(lang.L("ship.action.create"), c.onCreateShipClassButton)
c.shipCreateButton.Disable()
c.loader = c.LoadShipClass
c.planetMatEntry = numeric.NewFloatEntry(10, c.onPlanetMatChange)
c.planetMatOverride = widget.NewCheck("", c.overridePlanetMat)
c.planetMatOverride.Disable()
c.planetLabel = widget.NewLabel("")
c.planetMassProdLabel = bareLabel("")
c.planetShipsProdLabel = bareLabel("")
c.planetProdContainer = container.NewHBox(
label(lang.L("planet.prod.mass")+":"),
fixedLabel(c.planetMassProdLabel, 80),
label(lang.L("planet.prod.ships")+":"),
fixedLabel(c.planetShipsProdLabel, 80),
)
c.planetProdContainer.Hide()
c.planetContainer = container.NewVBox(
widget.NewSeparator(),
container.NewHBox(c.planetLabel),
rowForItem(lang.L("planet.mat")+":", floatEntry(c.planetMatEntry, 100), c.planetMatOverride),
c.planetProdContainer,
)
c.planetContainer.Hide()
c.shipSelector = widget.NewSelectEntry(nil)
c.shipSelector.OnChanged = c.onShipSelectorChange
c.shipDriveEntry = numeric.NewFloatEntry(7, c.onShipDriveChange)
c.shipWeaponsEntry = numeric.NewFloatEntry(7, c.onShipWeaponsChange)
c.shipArmamentEntry = numeric.NewIntEntry(7, c.onShipArmamentChange)
c.shipShieldsEntry = numeric.NewFloatEntry(7, c.onShipShieldsChange)
c.shipCargoEntry = numeric.NewFloatEntry(7, c.onShipCargoChange)
c.playerDrivesTechEntry = numeric.NewFloatEntry(7, c.onDrivesTechChange)
c.playerWeaponsTechEntry = numeric.NewFloatEntry(7, c.onWeaponsTechChange)
c.playerShieldsTechEntry = numeric.NewFloatEntry(7, c.onShieldsTechChange)
c.playerCargoTechEntry = numeric.NewFloatEntry(7, c.onCargoTechChange)
c.massEntry = numeric.NewFloatEntry(7, c.onMassChange)
c.speedEntry = numeric.NewFloatEntry(7, c.onSpeedChange)
c.attackEntry = numeric.NewFloatEntry(7, c.onAttackChange)
c.defenseEntry = numeric.NewFloatEntry(7, c.onDefenseChange)
c.cargoLoadEntry = numeric.NewFloatEntry(7, c.onCargoLoadChange)
c.drivesTechOverride = widget.NewCheck("", c.overrideDrivesTech)
c.drivesTechOverride.Disable()
c.weaponsTechOverride = widget.NewCheck("", c.overrideWeaponsTech)
c.weaponsTechOverride.Disable()
c.shieldsTechOverride = widget.NewCheck("", c.overrideShieldsTech)
c.shieldsTechOverride.Disable()
c.cargoTechOverride = widget.NewCheck("", c.overrideCargoTech)
c.cargoTechOverride.Disable()
c.massOverride = widget.NewCheck("", c.overrideMass)
c.massOverride.Disable()
c.speedOverride = widget.NewCheck("", c.overrideSpeed)
c.speedOverride.Disable()
c.attackOverride = widget.NewCheck("", c.overrideAttack)
c.attackOverride.Disable()
c.defenseOverride = widget.NewCheck("", c.overrideDefense)
c.defenseOverride.Disable()
c.cargoLoadMaximize = widget.NewCheck(lang.L("label.max"), c.maximizeCargoLoad)
c.cargoLoadMaximize.SetChecked(true)
createShip := container.NewBorder(
nil, // top
nil, // bottom
nil, // left
c.shipCreateButton, // right
c.shipSelector, // center
)
c.CanvasObject = container.NewVBox(
container.NewPadded(createShip),
widget.NewSeparator(),
rowForTech(lang.L("tech.d")+":",
c.shipDriveEntry, floatEntry(c.playerDrivesTechEntry, 80), c.drivesTechOverride),
rowForWeapons(lang.L("tech.w")+":",
c.shipArmamentEntry, c.shipWeaponsEntry, floatEntry(c.playerWeaponsTechEntry, 80), c.weaponsTechOverride),
rowForTech(lang.L("tech.s")+":",
c.shipShieldsEntry, floatEntry(c.playerShieldsTechEntry, 80), c.shieldsTechOverride),
rowForTech(lang.L("tech.c")+":",
c.shipCargoEntry, floatEntry(c.playerCargoTechEntry, 80), c.cargoTechOverride),
widget.NewSeparator(),
rowForItem(lang.L("ship.load")+":",
floatEntry(c.cargoLoadEntry, 80), c.cargoLoadMaximize),
rowForItem(lang.L("ship.mass")+":",
floatEntry(c.massEntry, 80), c.massOverride),
rowForItem(lang.L("ship.speed")+":",
floatEntry(c.speedEntry, 80), c.speedOverride),
rowForItem(lang.L("ship.attack")+":",
floatEntry(c.attackEntry, 80), c.attackOverride),
rowForItem(lang.L("ship.defense")+":",
floatEntry(c.defenseEntry, 80), c.defenseOverride),
c.planetContainer,
)
c.Init(opts...)
return c
}
func (c *Calculator) Init(opts ...CalculatorOpt) {
for i := range opts {
opts[i](c)
}
c.playerDrivesTechEntry.SetOrigin(c.playerDrivesTech)
c.playerWeaponsTechEntry.SetOrigin(c.playerWeaponsTech)
c.playerShieldsTechEntry.SetOrigin(c.playerShieldsTech)
c.playerCargoTechEntry.SetOrigin(c.playerCargoTech)
c.CanvasObject.Show()
}
func (c *Calculator) Refresh() {
c.validate()
c.CanvasObject.Refresh()
}
func (c *Calculator) RegisterClasses(shipClass ...ShipClass) {
c.knownClasses = shipClass
names := make([]string, len(c.knownClasses))
for i := range c.knownClasses {
names[i] = c.knownClasses[i].Name
}
slices.Sort(names)
c.shipSelector = widget.NewSelectEntry(names)
c.shipSelector.OnChanged = c.onShipSelectorChange
}
func (c *Calculator) onCreateShipClassButton() {
if c.onCreateHandler == nil || !c.Valid {
return
}
}
func (c *Calculator) validate() {
fyne.Do(func() {
c.validateMu.Lock()
err := c.validateEntries()
c.Valid = err == nil
if err != nil {
} else {
}
c.shipClassNameValidate()
c.validateMu.Unlock()
})
}
func (c *Calculator) validateEntries() (err error) {
defer func() {
if err != nil {
c.cargoLoadEntry.Clear()
if !c.massOverride.Checked {
c.massEntry.Clear()
}
if !c.speedOverride.Checked {
c.speedEntry.Clear()
}
if !c.attackOverride.Checked {
c.attackEntry.Clear()
}
if !c.defenseOverride.Checked {
c.defenseEntry.Clear()
}
// c.planetProdContainer.Hide()
}
}()
drive, ok := c.shipDriveEntry.Value()
if !ok {
err = errors.New("Parameter Drive is not valid")
return
}
driveTech, ok := c.playerDrivesTechEntry.Value()
if !ok {
err = errors.New("Drive tech level is not valid")
return
}
armament, ok := c.shipArmamentEntry.Value()
if !ok {
err = errors.New("Parameter Armament is not valid")
return
}
weapons, ok := c.shipWeaponsEntry.Value()
if !ok {
err = errors.New("Parameter Weapons is not valid")
return
}
weaponsTech, ok := c.playerWeaponsTechEntry.Value()
if !ok {
err = errors.New("Weapons tech level is not valid")
return
}
shields, ok := c.shipShieldsEntry.Value()
if !ok {
err = errors.New("Parameter Shields is not valid")
return
}
shieldsTech, ok := c.playerShieldsTechEntry.Value()
if !ok {
err = errors.New("Shields tech level is not valid")
return
}
cargo, ok := c.shipCargoEntry.Value()
if !ok {
err = errors.New("Parameter Cargo is not valid")
return
}
cargoTech, ok := c.playerCargoTechEntry.Value()
if !ok {
err = errors.New("Cargo tech level is not valid")
return
}
err = calc.ValidateShipTypeValues(drive, armament, weapons, shields, cargo)
if err != nil {
return
}
var cargoLoad float64
if c.cargoLoadMaximize.Checked {
cargoLoad = calc.CargoCapacity(cargo, cargoTech)
c.cargoLoadEntry.SetOrigin(cargoLoad)
} else if cargoLoad, ok = c.cargoLoadEntry.Value(); !ok {
err = errors.New("Cargo load value is not valid")
return
}
emptyMass, ok := calc.EmptyMass(drive, weapons, uint(armament), shields, cargo)
if !ok {
err = errors.New("Unable to calculate empty mass (check armament and weapons)")
return
}
fullMass := calc.FullMass(emptyMass, cargoLoad)
speed := calc.Speed(calc.DriveEffective(drive, driveTech), fullMass)
effectiveAttack := calc.EffectiveAttack(weapons, weaponsTech)
effectiveDefense := calc.EffectiveDefence(shields, shieldsTech, fullMass)
c.massEntry.SetOrigin(emptyMass)
c.speedEntry.SetOrigin(speed)
c.attackEntry.SetOrigin(effectiveAttack)
c.defenseEntry.SetOrigin(effectiveDefense)
planetMat, ok := c.planetMatEntry.Value()
if !ok {
// c.planetProdContainer.Hide()
} else {
massProd := calc.PlanetProduceShipMass(c.l, planetMat, c.res)
c.planetMassProdLabel.SetText(strconv.FormatFloat(util.Fixed3(massProd), 'f', -1, 64))
ships := 0.
if emptyMass > 0 {
ships = massProd / emptyMass
}
c.planetShipsProdLabel.SetText(strconv.FormatFloat(util.Fixed3(ships), 'f', -1, 64))
c.planetProdContainer.Show()
}
return
}
func (c *Calculator) onOriginInputChange(cb *widget.Check, e *numeric.FloatEntry) {
if e == nil {
return
}
if cb != nil {
cb.Checked = e.Overriden()
if !cb.Checked {
cb.Disable()
} else {
cb.Enable()
}
}
c.onFloatEntryChange(e)
}
func (c *Calculator) onFloatEntryChange(e *numeric.FloatEntry) {
if e == nil {
return
}
e.Validate()
c.validate()
}
func (c *Calculator) onIntEntryChange(e *numeric.IntEntry) {
if e == nil {
return
}
e.Validate()
c.validate()
}
func (c *Calculator) overrideChecked(cb *widget.Check, e *numeric.FloatEntry) {
if cb == nil || e == nil {
return
}
if !cb.Checked {
e.Reset()
cb.Disable()
}
}
func (c *Calculator) onShipDriveChange(string) {
c.onFloatEntryChange(c.shipDriveEntry)
}
func (c *Calculator) onShipArmamentChange(string) {
defer c.onIntEntryChange(c.shipArmamentEntry)
if weapons, ok := c.shipWeaponsEntry.Value(); !ok || !c.shipWeaponsEntry.Valid {
return
} else if armament, ok := c.shipArmamentEntry.Value(); !ok || !c.shipArmamentEntry.Valid {
return
} else if armament > 0 && weapons == 0 {
c.shipWeaponsEntry.SetOrigin(1.0)
} else if armament == 0 && weapons > 0 {
c.shipWeaponsEntry.SetOrigin(0.0)
}
}
func (c *Calculator) onShipWeaponsChange(string) {
defer c.onFloatEntryChange(c.shipWeaponsEntry)
if weapons, ok := c.shipWeaponsEntry.Value(); !ok || !c.shipWeaponsEntry.Valid {
return
} else if armament, ok := c.shipArmamentEntry.Value(); !ok || !c.shipArmamentEntry.Valid {
return
} else if weapons > 0 && armament == 0 {
c.shipArmamentEntry.SetOrigin(1)
} else if weapons == 0 && armament > 0 {
c.shipArmamentEntry.SetOrigin(0)
}
}
func (c *Calculator) onShipShieldsChange(string) {
c.onFloatEntryChange(c.shipShieldsEntry)
}
func (c *Calculator) onShipCargoChange(string) {
c.onFloatEntryChange(c.shipCargoEntry)
}
func (c *Calculator) onDrivesTechChange(string) {
c.onOriginInputChange(c.drivesTechOverride, c.playerDrivesTechEntry)
}
func (c *Calculator) overrideDrivesTech(bool) {
c.overrideChecked(c.drivesTechOverride, c.playerDrivesTechEntry)
}
func (c *Calculator) onWeaponsTechChange(string) {
c.onOriginInputChange(c.weaponsTechOverride, c.playerWeaponsTechEntry)
}
func (c *Calculator) overrideWeaponsTech(bool) {
c.overrideChecked(c.weaponsTechOverride, c.playerWeaponsTechEntry)
}
func (c *Calculator) onShieldsTechChange(string) {
c.onOriginInputChange(c.shieldsTechOverride, c.playerShieldsTechEntry)
}
func (c *Calculator) overrideShieldsTech(bool) {
c.overrideChecked(c.shieldsTechOverride, c.playerShieldsTechEntry)
}
func (c *Calculator) onCargoTechChange(string) {
c.onOriginInputChange(c.cargoTechOverride, c.playerCargoTechEntry)
}
func (c *Calculator) overrideCargoTech(bool) {
c.overrideChecked(c.cargoTechOverride, c.playerCargoTechEntry)
}
func (c *Calculator) onCargoLoadChange(string) {
c.onFloatEntryChange(c.cargoLoadEntry)
}
func (c *Calculator) onMassChange(string) {
c.onOriginInputChange(c.massOverride, c.massEntry)
}
func (c *Calculator) overrideMass(bool) {
c.overrideChecked(c.massOverride, c.massEntry)
}
func (c *Calculator) onSpeedChange(string) {
c.onOriginInputChange(c.speedOverride, c.speedEntry)
}
func (c *Calculator) overrideSpeed(bool) {
c.overrideChecked(c.speedOverride, c.speedEntry)
}
func (c *Calculator) onAttackChange(string) {
c.onOriginInputChange(c.attackOverride, c.attackEntry)
}
func (c *Calculator) overrideAttack(bool) {
c.overrideChecked(c.attackOverride, c.attackEntry)
}
func (c *Calculator) onDefenseChange(string) {
c.onOriginInputChange(c.defenseOverride, c.defenseEntry)
}
func (c *Calculator) overrideDefense(bool) {
c.overrideChecked(c.defenseOverride, c.defenseEntry)
}
func (c *Calculator) maximizeCargoLoad(bool) {
c.validate()
}
func (c *Calculator) onPlanetMatChange(string) {
c.onOriginInputChange(c.planetMatOverride, c.planetMatEntry)
}
func (c *Calculator) overridePlanetMat(bool) {
c.overrideChecked(c.planetMatOverride, c.planetMatEntry)
}
func (c *Calculator) onShipSelectorChange(v string) {
i, ok := c.shipClassNameValidate()
if i < 0 || !ok || c.loader == nil {
return
}
c.loader(
c.knownClasses[i].Name,
c.knownClasses[i].Drive,
c.knownClasses[i].Armament,
c.knownClasses[i].Weapons,
c.knownClasses[i].Shields,
c.knownClasses[i].Cargo,
)
}
func (c *Calculator) shipClassNameValidate() (int, bool) {
var canCreateShip bool
defer func() {
if canCreateShip && c.Valid {
c.shipCreateButton.Enable()
} else {
c.shipCreateButton.Disable()
}
}()
name, canCreateShip := util.ValidateTypeName(c.shipSelector.Text)
if canCreateShip {
c.shipSelector.Text = name
}
i := slices.IndexFunc(c.knownClasses, func(v ShipClass) bool { return v.Name == name })
canCreateShip = canCreateShip && i < 0
return i, canCreateShip
}
func (c *Calculator) LoadShipClass(n string, D float64, A uint, W float64, S float64, C float64) {
c.shipDriveEntry.SetOrigin(D)
c.shipArmamentEntry.SetOrigin(int(A))
c.shipWeaponsEntry.SetOrigin(W)
c.shipShieldsEntry.SetOrigin(S)
c.shipCargoEntry.SetOrigin(C)
}
func rowForItem(l string, entry, override fyne.CanvasObject) fyne.CanvasObject {
i := []fyne.CanvasObject{label(l), entry}
if override != nil {
i = append(i, override)
}
return container.NewHBox(i...)
}
func rowForTech(l string, shipEntry, techEntry, btn fyne.CanvasObject) fyne.CanvasObject {
return container.NewHBox(
label(l),
floatEntry(shipEntry, 115),
widget.NewLabel("@"),
techEntry,
btn,
)
}
func rowForWeapons(l string, armamentEntry, weaponsEntry, techEntry, btn fyne.CanvasObject) fyne.CanvasObject {
return container.NewHBox(
label(l),
intEntry(armamentEntry, 35),
floatEntry(weaponsEntry, 75),
widget.NewLabel("@"),
techEntry,
btn,
)
}
func label(l string) fyne.CanvasObject {
return fixedLabel(bareLabel(l), 110)
}
func fixedLabel(w *widget.Label, width float32) fyne.CanvasObject {
s := container.NewHScroll(w)
s.SetMinSize(fyne.NewSize(width, 1))
return s
}
func bareLabel(l string) *widget.Label {
w := widget.NewLabelWithStyle(l, fyne.TextAlignTrailing, fyne.TextStyle{Monospace: true, Symbol: false})
w.Selectable = false
w.Truncation = fyne.TextTruncateOff
return w
}
func intEntry(content fyne.CanvasObject, width float32) fyne.CanvasObject {
s := container.NewHScroll(content)
s.SetMinSize(fyne.NewSize(width, 1))
return s
}
func floatEntry(content fyne.CanvasObject, width float32) fyne.CanvasObject {
s := container.NewHScroll(content)
s.SetMinSize(fyne.NewSize(width, 1))
return s
}
+16
View File
@@ -0,0 +1,16 @@
package calculator
import (
"fyne.io/fyne/v2/lang"
)
func (c *Calculator) UnloadPlanet() {
c.planetContainer.Hide()
}
func (c *Calculator) LoadPlanet(name string, number uint, L, Mat, Res float64) {
c.l, c.mat, c.res = L, Mat, Res
c.planetLabel.SetText(lang.L("planet.title", map[string]any{"Number": number, "Name": name}))
c.planetMatEntry.SetOrigin(Mat)
c.planetContainer.Show()
}
+185 -10
View File
@@ -1,30 +1,82 @@
package numeric
import (
"galaxy/client/widget/validator"
"galaxy/util"
"strconv"
"strings"
"unicode/utf8"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/driver/mobile"
"fyne.io/fyne/v2/widget"
)
type numericalEntry struct {
type FloatEntry struct {
widget.Entry
origin float64
MaxValue float64
maxSize uint
validator fyne.StringValidator
Valid bool
}
func NewNumericalEntry() *numericalEntry {
entry := &numericalEntry{}
entry.ExtendBaseWidget(entry)
return entry
type IntEntry struct {
widget.Entry
origin uint
MaxValue uint
maxSize uint
validator fyne.StringValidator
Valid bool
}
func (e *numericalEntry) TypedRune(r rune) {
if (r >= '0' && r <= '9') || r == '.' || r == ',' {
e.Entry.TypedRune(r)
func NewFloatEntry(maxSize uint, onChanged func(string)) *FloatEntry {
e := &FloatEntry{maxSize: maxSize, validator: validator.FloatEntryValidator}
e.ExtendBaseWidget(e)
e.Entry.Scroll = fyne.ScrollNone
e.Entry.TextStyle = fyne.TextStyle{Monospace: true}
// e.Validator = validator.FloatEntryValidator
// e.AlwaysShowValidationError = true
e.Entry.ActionItem = nil
e.SetOrigin(0)
e.Validate()
e.Entry.OnChanged = onChanged
return e
}
func NewIntEntry(maxSize uint, onChanged func(string)) *IntEntry {
e := &IntEntry{maxSize: maxSize, validator: validator.IntEntryValidator}
e.ExtendBaseWidget(e)
e.Entry.Scroll = fyne.ScrollNone
e.Entry.TextStyle = fyne.TextStyle{Monospace: true}
// e.Validator = validator.IntEntryValidator
// e.AlwaysShowValidationError = true
e.Entry.ActionItem = nil
e.SetOrigin(0)
e.Validate()
e.Entry.OnChanged = onChanged
return e
}
func (e *FloatEntry) CreateRenderer() fyne.WidgetRenderer {
r := e.Entry.CreateRenderer()
return r
}
func (e *FloatEntry) TypedRune(r rune) {
if !((r >= '0' && r <= '9') || r == '.') {
return
}
if !lengthBelowLimit(e.Entry.Text, e.maxSize) && e.Entry.SelectedText() == "" {
return
}
if r == '.' && strings.Contains(e.Entry.Text, ".") {
return
}
e.Entry.TypedRune(r)
}
func (e *numericalEntry) TypedShortcut(shortcut fyne.Shortcut) {
func (e *FloatEntry) TypedShortcut(shortcut fyne.Shortcut) {
paste, ok := shortcut.(*fyne.ShortcutPaste)
if !ok {
e.Entry.TypedShortcut(shortcut)
@@ -37,6 +89,129 @@ func (e *numericalEntry) TypedShortcut(shortcut fyne.Shortcut) {
}
}
func (e *numericalEntry) Keyboard() mobile.KeyboardType {
func (e *FloatEntry) Keyboard() mobile.KeyboardType {
return mobile.NumberKeyboard
}
func (e *FloatEntry) SetOrigin(v float64) {
if v < 0 {
return
}
e.origin = v
e.Reset()
}
func (e *FloatEntry) Reset() {
e.SetValue(e.origin)
}
func (e *FloatEntry) Clear() {
onChanged := e.Entry.OnChanged
e.Entry.OnChanged = nil
e.Entry.SetText("")
e.Entry.OnChanged = onChanged
}
func (e *FloatEntry) SetValue(v float64) {
if v < 0 {
return
}
e.Entry.SetText(strconv.FormatFloat(util.Fixed3(v), 'f', -1, 64))
}
func (e *FloatEntry) Value() (float64, bool) {
if v, err := validator.ParseFloat(e.Entry.Text); err != nil {
return 0, false
} else {
return v, true
}
}
func (e *FloatEntry) Overriden() bool {
if v, ok := e.Value(); !ok {
return false
} else {
return util.Fixed3(v) != util.Fixed3(e.origin)
}
}
func (e *FloatEntry) Validate() {
if e.validator == nil {
return
}
err := e.validator(e.Entry.Text)
e.Valid = err == nil
}
func (e *IntEntry) TypedRune(r rune) {
if r >= '0' && r <= '9' {
if lengthBelowLimit(e.Entry.Text, e.maxSize) || e.Entry.SelectedText() != "" {
e.Entry.TypedRune(r)
}
}
}
func (e *IntEntry) TypedShortcut(shortcut fyne.Shortcut) {
paste, ok := shortcut.(*fyne.ShortcutPaste)
if !ok {
e.Entry.TypedShortcut(shortcut)
return
}
content := paste.Clipboard.Content()
if _, err := strconv.ParseInt(content, 10, 64); err == nil {
e.Entry.TypedShortcut(shortcut)
}
}
func (e *IntEntry) Keyboard() mobile.KeyboardType {
return mobile.NumberKeyboard
}
func (e *IntEntry) SetOrigin(v int) {
if v < 0 {
return
}
e.origin = uint(v)
e.Reset()
}
func (e *IntEntry) Reset() {
e.SetValue(int(e.origin))
}
func (e *IntEntry) SetValue(v int) {
if v < 0 {
return
}
e.Entry.SetText(strconv.Itoa(v))
}
func (e *IntEntry) Value() (int, bool) {
if v, err := validator.ParseInt(e.Entry.Text); err != nil {
return 0, false
} else {
return v, true
}
}
func (e *IntEntry) Overriden() bool {
if v, ok := e.Value(); !ok {
return false
} else {
return v != int(e.origin)
}
}
func (e *IntEntry) Validate() {
if e.validator == nil {
return
}
err := e.validator(e.Entry.Text)
e.Valid = err == nil
}
func lengthBelowLimit(s string, max uint) bool {
return utf8.RuneCountInString(s) < int(max)
}
+68
View File
@@ -2,11 +2,26 @@ package validator
import (
"errors"
"fmt"
"strconv"
"fyne.io/fyne/v2"
)
type floatValidator func(float64) error
var (
FloatEntryValidator = numericEntryValidator(
nonNegativeValidator,
minOrZeroValueValidator(1.),
)
IntEntryValidator = numericEntryValidator(
intValidator,
nonNegativeValidator,
minOrZeroValueValidator(1.),
)
)
func NewStackValidator(first fyne.StringValidator, rest ...fyne.StringValidator) fyne.StringValidator {
if first == nil {
panic("first validator cannot be nil")
@@ -43,6 +58,44 @@ func NewMutualValidator(other func() float64, valid func(float64) bool) fyne.Str
}
}
func numericEntryValidator(other ...floatValidator) fyne.StringValidator {
return func(s string) error {
v, err := ParseFloat(s)
if err != nil {
return errors.New("not a float value")
}
for i := range other {
if err := other[i](v); err != nil {
return err
}
}
return nil
}
}
func nonNegativeValidator(v float64) error {
if v < 0 {
return errors.New("value must be greater of equal to zero")
}
return nil
}
func intValidator(v float64) error {
if float64(int(v)) != v {
return errors.New("value must be an integer")
}
return nil
}
func minOrZeroValueValidator(min float64) floatValidator {
return func(f float64) error {
if f > 0 && f < min {
return fmt.Errorf("value must be zero or >= %f", min)
}
return nil
}
}
func FloatValueValidator(s string) error {
if _, err := ParseFloat(s); err != nil {
return errors.New("not a float value")
@@ -50,6 +103,21 @@ func FloatValueValidator(s string) error {
return nil
}
func IntValueValidator(s string) error {
if _, err := ParseInt(s); err != nil {
return errors.New("not an integer value")
}
return nil
}
func ParseFloat(s string) (float64, error) {
return strconv.ParseFloat(s, 64)
}
func ParseInt(s string) (int, error) {
if v, err := strconv.ParseInt(s, 10, 64); err != nil {
return 0, err
} else {
return int(v), nil
}
}
+22 -24
View File
@@ -415,6 +415,13 @@ func (LightTheme) PointClassOverride(class PointClassID) (StyleOverride, bool) {
PointRadiusPx: new(3.5),
}, true
case PointClassUnidentifiedPlanet:
// soft orange
return StyleOverride{
FillColor: cRGBA(192, 192, 192, 255),
PointRadiusPx: new(2.5),
}, true
default:
return StyleOverride{}, false
}
@@ -457,15 +464,7 @@ func (LightTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool)
case CircleClassDefault:
return StyleOverride{}, false
case CircleClassHome:
// teal-ish, a bit stronger stroke
return StyleOverride{
FillColor: cRGBA(32, 161, 145, 50),
StrokeColor: cRGBA(32, 161, 145, 210),
StrokeWidthPx: new(2.5),
}, true
case CircleClassAcquired:
case CircleClassLocalPlanet:
// blue
return StyleOverride{
FillColor: cRGBA(70, 108, 196, 45),
@@ -473,7 +472,7 @@ func (LightTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool)
StrokeWidthPx: new(2.2),
}, true
case CircleClassOccupied:
case CircleClassOthersPlanet:
// orange
return StyleOverride{
FillColor: cRGBA(222, 142, 70, 50),
@@ -481,7 +480,7 @@ func (LightTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool)
StrokeWidthPx: new(2.2),
}, true
case CircleClassFree:
case CircleClassFreePlanet:
// green
return StyleOverride{
FillColor: cRGBA(76, 171, 107, 45),
@@ -574,6 +573,12 @@ func (*DarkTheme) PointClassOverride(class PointClassID) (StyleOverride, bool) {
PointRadiusPx: new(3.5),
}, true
case PointClassUnidentifiedPlanet:
return StyleOverride{
FillColor: cRGBA(192, 192, 192, 255),
PointRadiusPx: new(2.5),
}, true
default:
return StyleOverride{}, false
}
@@ -615,30 +620,23 @@ func (*DarkTheme) CircleClassOverride(class CircleClassID) (StyleOverride, bool)
case CircleClassDefault:
return StyleOverride{}, false
case CircleClassHome:
case CircleClassLocalPlanet:
return StyleOverride{
FillColor: nil, // cRGBA(120, 214, 198, 255),
StrokeColor: cRGBA(120, 214, 198, 255),
StrokeWidthPx: new(2.5),
}, true
case CircleClassAcquired:
return StyleOverride{
FillColor: nil, // cRGBA(155, 175, 235, 255),
FillColor: cRGBA(155, 175, 235, 255),
StrokeColor: cRGBA(155, 175, 235, 255),
StrokeWidthPx: new(2.2),
}, true
case CircleClassOccupied:
case CircleClassOthersPlanet:
return StyleOverride{
FillColor: nil, // cRGBA(245, 178, 120, 255),
FillColor: cRGBA(245, 178, 120, 255),
StrokeColor: cRGBA(245, 178, 120, 255),
StrokeWidthPx: new(2.2),
}, true
case CircleClassFree:
case CircleClassFreePlanet:
return StyleOverride{
FillColor: nil, // cRGBA(132, 219, 162, 255),
FillColor: cRGBA(132, 219, 162, 255),
StrokeColor: cRGBA(132, 219, 162, 255),
StrokeWidthPx: new(2.2),
}, true
+8 -8
View File
@@ -688,6 +688,8 @@ const (
PointClassTrackIncoming
// PointClassTrackOutgoing marks a point as an outgoing track marker.
PointClassTrackOutgoing
// PointClassUnidentifiedPlanet marks an unidentified planet without visivle size.
PointClassUnidentifiedPlanet
)
// LineClassID classifies Line primitives for theme-level style overrides.
@@ -711,14 +713,12 @@ type CircleClassID uint8
const (
// CircleClassDefault selects the theme's default circle styling.
CircleClassDefault CircleClassID = iota
// CircleClassHome marks a circle as a home-world area.
CircleClassHome
// CircleClassAcquired marks a circle as an acquired world area.
CircleClassAcquired
// CircleClassOccupied marks a circle as an occupied world area.
CircleClassOccupied
// CircleClassFree marks a circle as a free world area.
CircleClassFree
// CircleClassLocalPlanet marks a circle as a player-owned planet.
CircleClassLocalPlanet
// CircleClassOthersPlanet marks a circle as an occupied planet.
CircleClassOthersPlanet
// CircleClassFreePlanet marks a circle as a free planet.
CircleClassFreePlanet
)
// PrimitiveID is a compact stable identifier for primitives stored in the World.